Web SecuritySeptember 21, 2026•9 min read

Mitigating Server-Side Request Forgery (SSRF) in Modern Node.js & Next.js Applications

Deep dive into SSRF attack vectors against cloud metadata endpoints (169.254.169.254) and internal networks. How to build bulletproof URL validation proxies.

RB
Rohit Bhatu — Lead Systems Architect
Mitigating Server-Side Request Forgery (SSRF) in Modern Node.js & Next.js Applications

1. What is SSRF and Why is it Catastrophic in the Cloud?

Server-Side Request Forgery occurs when an attacker induces a server to send a crafted HTTP request to an unintended destination, such as internal microservices, private VPC endpoints, or cloud metadata instances.

In AWS, GCP, and Azure environments, internal metadata endpoints (like `http://169.254.169.254/latest/meta-data/`) allow unauthenticated requests from running instances to fetch temporary IAM credentials, leading to total cloud account takeovers.

2. The Flaw of Simple URL Parsing and Regex

Many developers attempt to prevent SSRF by checking whether the URL contains words like `localhost` or `127.0.0.1` using regex. This is completely ineffective against decimal representations (e.g. `2130706433`), hexadecimal IPs (`0x7f000001`), IPv6 loopbacks (`[::1]`), and DNS rebinding.

In a DNS rebinding attack, an attacker configures a domain name whose DNS record resolves to a public IP on the first check, but changes its TTL to 0 and resolves to `169.254.169.254` or `10.0.0.1` milliseconds later when the fetch occurs.

Robust IP resolution and private range boundary validationtypescript
import dns from "node:dns/promises";
import net from "node:net";

const PRIVATE_SUBNETS = [
  /^127\./,               // Loopback
  /^10\./,                // Class A private
  /^172\.(1[6-9]|2[0-9]|3[0-1])\./, // Class B private
  /^192\.168\./,          // Class C private
  /^169\.254\./,          // Cloud Link-Local Metadata
  /^0\./,                 // Current network
];

export async function validateOutboundUrl(targetUrl: string): Promise<string> {
  const parsed = new URL(targetUrl);

  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    throw new Error("Protocol not permitted. Only HTTP and HTTPS allowed.");
  }

  // Resolve DNS to verify underlying IP addresses
  const records = await dns.lookup(parsed.hostname, { all: true });

  for (const { address } of records) {
    if (net.isIPv4(address)) {
      for (const subnet of PRIVATE_SUBNETS) {
        if (subnet.test(address)) {
          throw new Error(`Access to private IP space (${address}) is blocked.`);
        }
      }
    } else if (net.isIPv6(address)) {
      if (address === "::1" || address.startsWith("fc00:") || address.startsWith("fe80:")) {
        throw new Error("Access to private IPv6 addresses is blocked.");
      }
    }
  }

  return targetUrl;
}

3. Enforcing Strict Redirect Limits

Even if the initial URL is public, an attacker can configure their public server to issue an HTTP 302 redirect pointing directly to `http://169.254.169.254`. Standard HTTP libraries like `fetch` follow redirects automatically.

Always disable automatic redirects in your fetch client, validate the `Location` header manually against your SSRF filter, and limit total redirects to a maximum of 3 hops.