Server-side request forgery — tracked by MITRE as CWE-918 — is what happens when an attacker gets your server to make an HTTP request on their behalf, to a destination they choose. In Node.js, that usually means a URL supplied by a user (a webhook callback, an image-import field, a PDF-renderer source) flows into fetch, axios, or http.request without validation. The consequences go far beyond an internal port scan: in 2019, an SSRF flaw in a misconfigured web application firewall let an attacker reach AWS's instance metadata service at 169.254.169.254, retrieve temporary IAM credentials, and pull data belonging to roughly 100 million Capital One customers. The attacker was arrested within weeks and convicted in 2022, but the pattern she exploited — an SSRF bug plus an unauthenticated metadata endpoint plus over-permissioned IAM credentials — is still present in plenty of production Node.js services today. AWS shipped IMDSv2, a session-oriented, token-gated version of that metadata service, in November 2019 specifically to blunt this attack class, yet IMDSv1 remains enabled by default on many older instances. This guide walks through how SSRF actually happens in Node.js code, why cloud metadata endpoints make it so dangerous, and which mitigations — allowlisting, redirect handling, and network egress controls — actually hold up.
How does SSRF happen in a typical Node.js app?
SSRF happens whenever a server-side HTTP client is handed a URL, host, or IP that originated from user input and is fetched without restriction. The classic case is a "fetch this image URL" or "verify this webhook" feature: the client sends {"url": "http://internal-admin/reset"}, the server calls axios.get(url) or fetch(url), and whatever is listening on that internal address responds as if the request came from a trusted, in-network caller — because it did. Node's fetch (built on undici, available by default since Node 18 and marked stable in Node 21) and axios both perform the request exactly as instructed, with no awareness that "internal-admin," "localhost," "10.0.0.5," or "169.254.169.254" are meaningfully different from a public API endpoint. The bug is rarely in the HTTP client itself; it's in application code trusting a string as a destination without first deciding which destinations are acceptable. PDF generators, URL preview/unfurl features, SSO metadata fetchers, and file-import-by-URL endpoints are the most common places this pattern shows up in real codebases.
Why is the 169.254.169.254 metadata endpoint the real prize?
The address 169.254.169.254 is the link-local IP that AWS, Azure, GCP, and DigitalOcean all use to serve instance metadata — including, on AWS, temporary IAM credentials for whatever role is attached to the EC2 instance. Any process on the instance can normally reach it, since it's designed for the OS and deployment tooling to self-configure. That's exactly what makes it attractive to an attacker who has found an SSRF bug: instead of scanning your internal network for interesting services, they issue one request to a fixed, well-known address and get back access-key-style credentials scoped to your cloud account. This is precisely the pivot used in the Capital One incident — the SSRF flaw itself only exposed an internal network; it was the unauthenticated IMDSv1 response that turned it into a data breach. AWS's IMDSv2, introduced in November 2019, requires a PUT request with a session token before any metadata GET succeeds, which defeats simple SSRF requests that can only issue a GET with attacker-controlled headers — but IMDSv2 has to be explicitly enforced per instance, and IMDSv1 access isn't blocked by default on every account.
Why doesn't a simple URL allowlist fully solve this?
An allowlist that checks the URL's hostname against approved domains is a necessary first step, but two well-documented bypasses defeat a naive implementation. The first is redirects: both axios (default maxRedirects: 5) and Node's fetch follow HTTP redirects automatically, so an attacker can point your allowlist check at https://trusted-partner.com/redirect?to=http://169.254.169.254/, pass validation on the first hop, and let the redirect chain land somewhere the allowlist never inspected. The second is DNS rebinding: a hostname can resolve to a safe public IP at validation time and then re-resolve to 127.0.0.1 or a private-range address by the time the actual connection is made, because DNS and the HTTP request are two separate steps with a gap between them. Real fixes address both: disable automatic redirects (maxRedirects: 0 in axios, redirect: 'manual' in fetch) and re-validate every hop, and pin the resolved IP at connection time — using a custom lookup function on Node's http/https agent — rather than trusting a hostname-only check performed once before the request is sent.
Are npm packages like ssrf-req-filter enough on their own?
Packages such as ssrf-req-filter and request-filtering-agent wrap Node's HTTP(S) agent to automatically block requests to private, loopback, and link-local ranges, and they're a reasonable baseline for catching the common case without hand-rolling agent logic. They are not a guaranteed fix, though: SSRF-guard libraries across the npm ecosystem have historically had their own gaps around edge-case IP representations — things like IPv6-mapped IPv4 addresses, decimal or octal IP encodings, and other non-canonical formats that a strict CIDR check can miss if the library doesn't normalize the address first. Treat these packages as one layer of defense, verify which encodings and redirect scenarios a given library's tests actually cover before relying on it, and keep the library current, since this is an area where new bypass techniques surface periodically. Pairing a library-level filter with allowlisting and infrastructure-level egress rules is safer than depending on any single control.
What does OWASP recommend as defense-in-depth?
The OWASP SSRF Prevention Cheat Sheet lays out layered controls rather than one silver bullet, because application-layer validation alone has repeatedly proven bypassable. At the code layer: allowlist destination hosts and IPs rather than blocklisting known-bad ones (blocklists miss encodings and internal ranges you forgot to enumerate), disable or strictly re-validate redirects, and explicitly reject requests resolving into 169.254.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, and 127.0.0.0/8. At the infrastructure layer: enforce egress rules with security groups or a network firewall so that even a successful SSRF request can't reach the metadata endpoint or internal services the application has no legitimate reason to call, and require IMDSv2 on every AWS instance so a stray GET request can't retrieve credentials even if it lands. None of these controls is sufficient in isolation — the Capital One breach involved a misconfigured WAF, an SSRF-vulnerable request path, and IMDSv1 all lining up at once — which is exactly why defense-in-depth, not a single fix, is the standard guidance.