SSRF
SSRF is the attack class where an outside user tricks a server into making requests on their behalf, usually to internal infrastructure that's unreachable from the public internet. Any service that fetches a URL provided by user input (including most CORS proxies, webhook validators, and link-preview generators) is a potential SSRF surface.
Why it's dangerous
The attacker's machine can't reach 169.254.169.254 (the AWS metadata service) or 10.0.0.5 (an internal Redis without auth). The server can. If the server will fetch any URL it's given, the attacker just asks it to fetch those internal URLs and returns the result. That's how cloud breaches happen.
Classic targets
- Cloud metadata endpoints:
169.254.169.254on AWS, GCP, Azure. Returns temporary IAM credentials. - Internal admin UIs: Kibana, Elasticsearch, Grafana, internal-only Jenkins, often deployed assuming "only the internal network can reach us".
- Unauthenticated data stores: Redis, Memcached, Mongo bound to
0.0.0.0"for convenience" inside a private network. - Local services:
localhostdebug endpoints, profilers, anything bound to 127.0.0.1. - Side-channel probing: sending many requests and timing the responses to map the internal network.
How a CORS proxy gets weaponized
// Innocent CORS proxy code (DO NOT DEPLOY)
app.get('/proxy', async (req, res) => {
const target = req.query.url;
const upstream = await fetch(target);
res.set('Access-Control-Allow-Origin', '*');
res.send(await upstream.text());
});
// Attacker:
GET /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
// Server happily fetches AWS creds and returns them.
The standard mitigations
- Resolve hostnames and check the IP against an allowlist or denylist before fetching. The check must happen on the resolved IP, not the hostname:
localtest.meresolves to127.0.0.1. Re-check every redirect hop, and if the check is a separate lookup before the fetch, remember that DNS rebinding can race it. - Reject private and reserved ranges: 0.0.0.0/8, 10/8, 100.64.0.0/10 (carrier-grade NAT), 127.0.0.0/8, 169.254.0.0/16, 172.16/12, 192.168/16, ::1, fc00::/7, fe80::/10.
- Reject non-HTTP schemes like
file://,gopher://,data://, which open additional protocol-confusion attacks. - Strip dangerous request headers: never forward incoming
Authorization,Cookie, or proxy-control headers to the upstream. - Limit response size and time so the proxy can't be used as a relay for terabyte exfiltration.
- Rate-limit per account: probing usually takes many requests, and per-key limits follow an attacker who rotates IP addresses.
corsproxy.dev refuses literal private, loopback, link-local and cloud-metadata targets, and hostnames that its DNS check resolves to them, with a 403 BLOCKED_HOST error. For the details and limits of that check, see the security writeup.