Server-Side Request Forgery (SSRF)
Also known as: SSRF, Server-side request forgery, Cloud metadata SSRF
A flaw where a server fetches a URL supplied by a user, letting that user steer the request toward internal services and cloud metadata endpoints that the server can reach but they cannot.
How it works
Server-side request forgery happens when an application takes a URL, host name or address from a user and makes an outbound request to it from the server, without constraining where that request may go. The server becomes a proxy that the user can aim.
The danger comes from position. A server sits inside the network and often has access that its users lack: internal admin panels, databases, service meshes and, in the cloud, an instance metadata service on a link-local address. A request that the server makes carries the server's network reach and sometimes its identity.
Common entry points are features that fetch on the user's behalf: link previews, image or avatar import, webhooks, PDF renderers, document converters and integrations. Naive checks fail in predictable ways, such as matching on a host name string that later resolves to a different address, or following a redirect to an internal target.
Defence is about constraining the fetcher rather than filtering strings. Permit only known destinations, resolve and validate the address that will actually be used, refuse internal and link-local ranges, require session-bound metadata access, and run the fetcher in a network segment that cannot reach what it does not need.
Walk through it
- 1Review the fetch endpoint
- 2Spot the tell in egress logs
- 3Review the fixed handler
- Scope what was reachable
- Contain, allowlist and enforce IMDSv2
A pull request adds a link-preview feature. Read the handler as a reviewer: where does the URL come from, and what stops the server from requesting it no matter where it points? Notice that nothing sits between the user input and the outbound call.
1app.get('/preview', async (req, res) => {2 const target = String(req.query.url);3 const upstream = await fetch(target); // server makes the call4 const html = await upstream.text();5 res.json({ title: extractTitle(html) });6});Spot it
- Outbound requests from an application server to 169.254.169.254 or other link-local addresses.
- Requests to loopback or RFC 1918 ranges from a service that normally talks only to public sites.
- A user-supplied URL parameter whose value is an IP literal, an unusual port, or a non-https scheme.
- Host names that resolve to internal addresses, or that change resolution between check and use.
- Metadata service access from a process that is not the instance's own agent, followed by unexpected use of the instance role.
Egress proxy
ts=2026-10-11T08:11:31Z src=preview-01 method=GET dst=169.254.169.254 path=/latest/meta-data/ status=200 category=link-local action=allowed
ts=2026-10-11T08:11:44Z src=preview-01 method=GET dst=10.0.4.17 port=8500 status=200 category=internal action=allowedApplication access log
ts=2026-10-11T08:11:31Z route=/preview user=anon query.url=http://169.254.169.254/latest/meta-data/ status=200splSplunk — proxy requests to link-local or private ranges from web tiers
index=proxy sourcetype=egress src IN (preview-*, web-*)
| where cidrmatch("169.254.0.0/16", dst) OR cidrmatch("10.0.0.0/8", dst) OR cidrmatch("172.16.0.0/12", dst) OR cidrmatch("192.168.0.0/16", dst) OR cidrmatch("127.0.0.0/8", dst)
| stats count by src, dst, uri_pathWeb tiers rarely have a legitimate reason to reach these ranges. Allowlist known service-to-service calls.
sigmaSigma — request to the cloud metadata address
detection:
selection:
cs-host|contains: '169.254.169.254'
condition: selection
level: highStop it
Allowlist the destinations the feature needs
Decide which hosts and schemes the feature legitimately calls and reject everything else. An allowlist holds up where a blocklist of bad addresses is bypassed by alternate encodings, redirects and new names.
Validate the resolved address, not the string
Resolve the name yourself, reject loopback, private and link-local results, then connect to that exact address so the name cannot resolve differently later. Do not follow redirects without applying the same checks.
Remove the prize and the path
Enforce IMDSv2 so metadata needs a session token, give instances least-privilege roles, and place fetchers in a segment whose egress rules deny internal and metadata destinations.
Allowlisted, address-validated fetch
Vulnerable
const upstream = await fetch(String(req.query.url));Hardened
const url = new URL(String(req.query.url));
if (url.protocol !== 'https:' || !ALLOWED_HOSTS.has(url.hostname)) throw new Error('not permitted');
const ip = await resolveOnce(url.hostname);
if (isPrivateOrLinkLocal(ip)) throw new Error('not permitted');
const upstream = await fetchPinned(url, ip, { redirect: 'manual' });resolveOnce, isPrivateOrLinkLocal and fetchPinned stand for your platform's DNS, range and connection helpers.
Require IMDSv2 on an AWS instance
Hardened
aws ec2 modify-instance-metadata-options \
--instance-id i-0123456789abcdef0 \
--http-tokens required \
--http-put-response-hop-limit 1A hop limit of 1 also stops containers behind the host from reaching the service.
- Enforce IMDSv2 (session-token metadata access) on every instance and scope instance roles to least privilege.
- Allow only https and an explicit host allowlist for any feature that fetches user-supplied URLs.
- Block loopback, RFC 1918, link-local and metadata ranges at the egress firewall for fetcher workloads.
- Disable or tightly control redirect following, and apply the same validation to each hop.
- Return generic errors and avoid echoing upstream response bodies back to the caller.
- Alert on any web-tier request to a metadata or internal address.
If it already happened
Disable or gate the vulnerable endpoint, block the metadata and internal ranges from the affected workload at the egress firewall, and enforce IMDSv2 on the instance.
Determine which internal addresses and metadata paths the service reached. Rotate any credentials the instance role could expose and review cloud audit logs for use of that role from unexpected places.
Ship the allowlist and address validation fix, redeploy, and confirm in egress logs that internal and link-local destinations are refused.
Add the egress detection permanently, inventory every feature that fetches user-supplied URLs, and add an SSRF check to code review and testing.
Check yourself
1. Why is a server-side feature that fetches a user-supplied URL risky?
2. Which log entry from a public-facing web server should most urgently alert an analyst?
3. Which control best reduces both the chance and the impact of SSRF against cloud metadata?