compiler-explorer / compiler-explorer/infra
Client IP is spoofable via X-Forwarded-For (trust proxy true, ALB open to the world)
- Dominant language
- Python
- Stars
- 434
- Forks
- 429
- Avg merge
- 4d 21h
- Merged PRs (30d)
- 38
Description
## Symptom
Papertrail shows request log lines like:
```
[ip-172-30-1-152] [amazon] warn: 127.0.0.0 "GET /wp-content/backups/data%2esql" 404
```
The "client" is logged as localhost. The CloudFront access logs for the same requests (2026-09-14, 16:07 to 16:39 UTC and again at 20:00 UTC) show the real source was four hosts in `185.177.72.0/24` running `curl/8.7.1`, each sending exactly 3001 vulnerability-scan requests (`.env`, `wp-json`, `terraform/modules.tf`, `.aws/config`, ...) against random subdomains (`noam.`, `taf.`, `newstudent.godbolt.org`), every one carrying a client-supplied header:
```
X-Forwarded-For: 127.0.0.1
```
CloudFront and the ALB both append to that header rather than replace it, so the app receives `127.0.0.1, , `.
Other sources in the same logs spoof too (`45.148.10.60` sending `127.0.0.1` during a wordpress scan), and corporate proxies such as Zscaler (`165.225.x`, `151.186.x`) legitimately add their internal client address as the leftmost entry, which is why those requests also log an IP that is not the peer.
## Cause
`compiler-explorer/lib/app/server-config.ts` sets `trust proxy` to `true`. Express then takes the leftmost `X-Forwarded-For` entry as `req.ip`, which is whatever the client chose to send. `lib/storage/s3.ts` (`storeItem`) goes further and reads the raw `X-Forwarded-For` header directly.
The infra side is what makes the app-side fix awkward: `terraform/security.tf` opens the ALB on 443 to `0.0.0.0/0` (rule `ALB_HttpsFromAnywhere`), so a request may arrive with two trusted hops (CloudFront, ALB) or one (ALB only). A fixed hop count in Express is only correct once the chain is deterministic. There is already a TODO in `security.tf` about restricting the ALB to the CloudFront origin-facing managed prefix list (`pl-3b927c52`).
## Impact
The IP we log, and anything keyed on `req.ip` (abuse tracing, short link creator IP in `storeItem`, any future rate limiting), is attacker-controlled. No availability impact: the scan itself was cheap 404s and did not move latency.
## Proposed fix
1. infra: restrict the ALB 443 ingress to the CloudFront managed prefix list so only CloudFront can reach it. Note the prefix list carries a weight of 55 rules against the security group's 60 rule quota, so check the remaining rules in that group (or ask for a quota bump). Keep an ingress path for the admin node if anyone relies on hitting the ALB directly for debugging.
2. compiler-explorer: set `trust proxy` to the exact hop count (2: CloudFront then ALB) or resolve the client from `CloudFront-Viewer-Address` if the origin request policy forwards it, and make `storeItem` use `req.ip` instead of the raw header.
Evidence: CloudFront logs under `s3://compiler-explorer-logs/cloudfront/` for `EFCZGUFIBB1UY.2026-09-14-16.*` and `-20.*`; the `x-forwarded-for` column carries the spoofed value.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Contributor guide
Research direction
Start with terraform/security.tf and its TODO about the CloudFront origin-facing prefix list, then read compiler-explorer/lib/app/server-config.ts and lib/storage/s3.ts, including storeItem. Confirm the ALB rule, remaining security-group quota, and any required admin path before choosing the trusted-hop or CloudFront-header approach. Done means direct ALB spoofing is prevented, client IP handling is no longer attacker-controlled, and storeItem uses the trusted request IP.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, express, terraform, typescript
- Domain
- backend, infrastructure, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100