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.
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.
Developers usually start with a denylist of hostnames, which attackers bypass in several ways:
2130706433), octal, IPv6-mapped (::ffff:127.0.0.1).10.0.0.5, or one that returns a public address on the first lookup and a private one on the second (DNS rebinding).302 Location: http://169.254.169.254/.@, and a regex on the string sees the wrong one.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.
The sample code applies the defenses in order:
new URL() so the hostname is extracted by a real parser, not a regex.https:; reject file:, gopher:, ftp: and plain http: where possible.redirect: "error") or follow them manually and re-run every check on each hop.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.
Application checks are the first layer, not the last:
# 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 enabledRun URL-fetching features in a separate container with an egress firewall that blocks private ranges:
iptables -A OUTPUT -d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -d 169.254.169.254 -j DROPGive 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.
Why is checking the hostname string against a denylist insufficient to prevent SSRF?
new URL(), allow only expected schemes and hosts, resolve DNS, reject private ranges, refuse redirects.Next lesson: Secure Coding Practices for Developers — the general habits that make every lesson so far second nature.