A URL field that can reach cloud credentials
A new 'import product image from URL' feature fetches whatever URL it's given — from inside the VPC, next to the instance metadata service.
- Weakness
- CWE-918 · Server-side request forgery · OWASP A10
- Target
POST /products/:id/image-from-urland the EC2/ECS metadata endpoints
Run these techniques only against the lab you own. Using them on systems you don't have permission to test is illegal.
The threat
01What SSRF is and why cloud makes it worse
Server-side request forgery happens when an application fetches a URL supplied by a user. The request comes FROM the server, so it can reach things the user can't: internal services, admin endpoints, and in the cloud, the instance METADATA SERVICE at a link-local address, which hands out the machine role's temporary credentials.
This isn't theoretical: an SSRF reaching the metadata service was central to the 2019 Capital One breach, which exposed data on roughly 100 million people. It's why AWS introduced IMDSv2.
What's at risk
- Whatever the instance or task role can do in AWS, available to anyone who can call the feature.
Detect
01Find user-controlled URLs reaching network calls
Semgrep's SSRF rules follow request data into
fetch,axios, andhttp.request. Every finding is a feature that needs an allowlist.terminal$ docker run --rm -v "$PWD:/src" semgrep/semgrep:1.128.0 semgrep scan --config p/ssrf -q /src/app── output ──app/server.js❯❱ javascript.express.security.audit.express-ssrf.express-ssrfThe following request uses user input to build its URL. This could lead toserver-side request forgery ...41┆ const img = await fetch(req.body.url);02Check whether instances still allow IMDSv1
IMDSv1 answers any plain GET, which is exactly what a naive SSRF can send. IMDSv2 requires a session token obtained with a PUT that includes a special header, which typical SSRF primitives can't produce. List instances that still accept v1.
terminal$ aws ec2 describe-instances --query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].[InstanceId,Tags[?Key=='Name']|[0].Value]" --output text── output ──i-0a1b2c3d4e5f60718 legacy-image-worker03Runtime: GuardDuty notices stolen instance credentials
If instance credentials are used from an IP outside AWS, GuardDuty raises
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS(Terraform course, Mission 7.3). Route it to the pager.
Defend
011. Require IMDSv2 with a hop limit of 1
http_tokens = requireddisables v1 entirely. A hop limit of 1 means the token response can't travel beyond the instance itself, so containers on bridge networks can't use it either unless intended. ECS tasks use their own credentials endpoint, but the same principle applies: scope what the app's role can do.Vulnerable
infra/probe.tfadd to filehcl resource "aws_instance" "worker" { # metadata_options not set → IMDSv1 allowed on older AMIs/defaults }Hardened
infra/probe.tfadd to filehcl resource "aws_instance" "worker" { metadata_options { http_tokens = "required" # IMDSv2 only http_put_response_hop_limit = 1 http_endpoint = "enabled" } }022. Allowlist destinations — and check the resolved IP
Accept only
https, only hosts on an allowlist (your CDN, known partners), and reject any hostname that RESOLVES to a private, loopback, or link-local address, because an attacker-controlled domain can point anywhere. Disable redirects (or re-validate each hop) and cap size and time. Libraries likessrf-req-filterimplement the IP checks at the connection level, which also defeats DNS rebinding.Vulnerable
server.jsadd to filejs const img = await fetch(req.body.url);Hardened
server.jsadd to filejs const ALLOWED_HOSTS = new Set(["images.acme-cdn.com", "partner-media.example.com"]); function assertAllowed(raw) { const url = new URL(raw); if (url.protocol !== "https:") throw new Error("https only"); if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error("host not allowed"); return url; } const url = assertAllowed(req.body.url); const img = await fetch(url, { redirect: "error", signal: AbortSignal.timeout(3000), // plus an agent that refuses private/link-local IPs at connect time });033. Restrict egress at the network layer
Services that fetch external content should have egress limited to what they need (an egress proxy with an allowlist, or security group and NACL rules). Even a missed code path then can't reach arbitrary internal addresses.
Verify
01Every instance requires IMDSv2; the allowlist rejects everything else
The instance query returns nothing. Unit tests for
assertAllowedcover: http scheme rejected, unknown host rejected, allowed host accepted, and an allowed-looking hostname resolving to a private IP rejected by the connection filter.terminal$ aws ec2 describe-instances --query "Reservations[].Instances[?MetadataOptions.HttpTokens=='optional'].InstanceId" --output text | wc -wnpm test -- ssrf── output ──0✔ rejects non-https URLs✔ rejects hosts outside the allowlist✔ accepts allowlisted CDN URLs✔ rejects allowlisted names resolving to private addresses4 passing
The concepts
Why blocklists fail for SSRF
Blocking '169.254.169.254' or 'localhost' as strings misses alternative encodings of the same address, IPv6 forms, DNS names that resolve to internal IPs, and redirects from an allowed host to an internal one. Robust defences are allowlists of destinations plus checking the IP actually connected to, combined with making the internal targets themselves safe (IMDSv2, authenticated internal services).
Your turn
How do you enforce IMDSv2 across a whole AWS account, including instances created by hand?
Interview questions
What is SSRF and how do you defend against it in the cloud?