Server-Side Request Forgery (SSRF)

Advanced
11 min

Server-Side Request Forgery (SSRF)

Many features need the server to fetch a URL: webhooks, "import from URL", link previews, image proxies. Server-Side Request Forgery (OWASP A10) happens when the attacker controls that URL and the server obediently requests something it should never touch: an internal admin panel, a database port, or the cloud metadata service that hands out credentials. The server has network access and trust the attacker lacks, and SSRF borrows both.

In this lesson you will learn how SSRF works, why naive URL checks fail, how to build a safe fetch function in Node.js, and which network-level controls limit the damage when application checks are bypassed.

How the Attack Works

Suppose an endpoint accepts { "imageUrl": "https://…" } and downloads the image. The attacker submits a URL that points inside your network instead:

| Target | Why it matters | |---|---| | http://127.0.0.1:6379/ or other local ports | Services bound to localhost assume anything that reaches them is trusted | | http://10.0.0.5/admin | Internal dashboards with no authentication "because they are not reachable" | | http://169.254.169.254/ | Cloud instance metadata, which can return temporary credentials for the instance role |

Even when the response is hidden from the attacker ("blind" SSRF), timing and error messages reveal which hosts exist, and requests with side effects succeed regardless.

Why Simple Checks Fail

Developers usually start with a denylist of hostnames, which attackers bypass in several ways:

  • Alternate encodings of the same address: decimal (2130706433), octal, IPv6-mapped (::ffff:127.0.0.1).
  • DNS tricks: a domain the attacker controls that resolves to 10.0.0.5, or one that returns a public address on the first lookup and a private one on the second (DNS rebinding).
  • Redirects: an allowed public URL that answers with 302 Location: http://169.254.169.254/.
  • Parser confusion: the real host hides after an @, and a regex on the string sees the wrong one.
javascript
new URL("https://images.example.com@evil.example.net/").hostname; // "evil.example.net"

Each bypass targets a check performed on the string. Robust defenses check the resolved destination and constrain what the request can do.

Building a Safe Fetch

The sample code applies the defenses in order:

  1. Parse with new URL() so the hostname is extracted by a real parser, not a regex.
  2. Allow only https:; reject file:, gopher:, ftp: and plain http: where possible.
  3. Prefer an allowlist of destination hosts. When the feature genuinely needs arbitrary hosts, fall through to the next checks.
  4. Resolve the hostname and reject private, loopback, link-local and IPv6-mapped ranges before connecting.
  5. Disable redirects (redirect: "error") or follow them manually and re-run every check on each hop.
  6. Set a timeout and a response size limit so the endpoint cannot be used to tie up the server.

For rebinding protection, connect to the IP you validated rather than letting the HTTP client resolve the name again; libraries such as undici support a custom lookup or connect hook for this.

Network-Level Defenses

Application checks are the first layer, not the last:

bash
# Require IMDSv2 on AWS instances: metadata needs a PUT-obtained token, which SSRF cannot usually perform aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 \ --http-tokens required --http-endpoint enabled

Run URL-fetching features in a separate container with an egress firewall that blocks private ranges:

bash
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP iptables -A OUTPUT -d 169.254.169.254 -j DROP

Give internal services their own authentication ("only reachable from inside" is not a control), and log every outbound request made on behalf of a user, including the resolved IP. A bypass of the application check then reaches nothing useful.

Quick Quiz
Question 1 of 2

Why is checking the hostname string against a denylist insufficient to prevent SSRF?

Key Takeaways

  • SSRF turns your server into a proxy for reaching internal services and cloud metadata.
  • String-based hostname checks are bypassed by encodings, DNS rebinding, redirects and parser confusion.
  • Parse with new URL(), allow only expected schemes and hosts, resolve DNS, reject private ranges, refuse redirects.
  • Add timeouts and size limits, and connect to the validated IP to defeat rebinding.
  • Enforce IMDSv2, egress firewalls and authentication on internal services so a bypass reaches nothing valuable.

Next lesson: Secure Coding Practices for Developers — the general habits that make every lesson so far second nature.

Server-Side Request Forgery (SSRF) - Cyber Security | CodeYourCraft | CodeYourCraft