SSRF Hardening in Node: Block Metadata and DNS Rebinding on Fetches

SSRF Hardening in Node: Block Metadata and DNS Rebinding on Fetches
26 views
This guide focuses on SSRF Hardening in Node for production systems, with practical trade-offs for reliability, security, and cost. User-supplied URLs (avatar, webhook, import from URL) turn your Node service into an HTTP client inside the VPC. Attackers aim at 169.254.169.254, link-local, and DNS rebinding. The unfair advantage is pinning the resolved address at connect time—deny private ranges after DNS, not just after parsing the hostname string.
⚡ TL;DR: Parse URL; resolve DNS yourself; reject private/link-local/metadata IPs; connect to the pinned address with Host header; block redirects to private targets; prefer egress proxy allowlists. Pair with Lambda Outbound Proxies and Node Permission Model.

SSRF Hardening in Node: production guidance

text
1) Attacker URL: http://evil.example/img  → resolves to 1.2.3.4 (public)  ✅ passes check
2) TTL expires / rebind → 169.254.169.254 at connect time               ❌ SSRF
typescript
// ✅ Pin address after resolution (sketch)
import dns from "node:dns/promises";
import net from "node:net";
import { Agent, fetch } from "undici";

const BLOCK = [
  /^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[0-1])\./, /^192\.168\./,
  /^169\.254\./, /^0\./, /^100\.64\./, /^::1$/, /^fc00:/i, /^fe80:/i,
];

function isBlockedIp(ip: string): boolean {
  return BLOCK.some((re) => re.test(ip));
}

export async function safeFetch(userUrl: string): Promise<Response> {
  const u = new URL(userUrl);
  if (!["http:", "https:"].includes(u.protocol)) throw new Error("scheme");
  const { address, family } = await dns.lookup(u.hostname, { verbatim: true });
  if (isBlockedIp(address)) throw new Error("ssrf_blocked");

  // Connect to pinned IP; send original Host
  const pinned = family === 6 ? `http://[${address}]${u.pathname}${u.search}`
                              : `http://${address}${u.pathname}${u.search}`;
  const httpsUrl = u.protocol === "https:"
    ? pinned.replace("http://", "https://")
    : pinned;

  return fetch(httpsUrl, {
    method: "GET",
    redirect: "manual", // ✅ inspect Location yourself
    headers: { Host: u.host },
    dispatcher: new Agent({ connect: { timeout: 2000 } }),
  });
}
typescript
// ❌ Trusting only URL.hostname string checks
if (userUrl.includes("169.254.169.254")) throw new Error("no");
await fetch(userUrl); // rebinding / DNS / redirects still bite

Redirects and IPv6 tricks

Follow redirects manually: re-run the IP check on every Location. Deny decimal/octal IP forms, DNS to @, and credentialed URLs. Prefer an egress proxy (Lambda Outbound Proxies) so the task role never reaches IMDS.
Control Stops
Pin + private IP deny Direct metadata / RFC1918
Manual redirects Open redirect to IMDS
IMDS hop limit / IMDSv2 Host metadata from task
Egress allowlist proxy Defense in depth

Closing checklist

✅ Dos – ✅ Resolve then deny private/link-local/metadata ranges – ✅ Pin IP for the TCP connect; set Host explicitly – ✅ Re-validate on every redirect hop – ✅ Force IMDSv2 + hop limit on EC2/ECS hosts – ✅ Default-deny egress except proxy / known peers ❌ Don’ts – ❌ Don’t fetch(userUrl) with auto-redirect – ❌ Don’t rely on string denylists alone – ❌ Don’t allow file:, gopher:, or weird schemes – ❌ Don’t give the app role broad ec2:Describe* just because SSRF “might need it”

Related reading

Last updated on September 11, 2026 Related: Node DNS Caching: Stop Micro-Outages From Brief Resolver Blips; Node.js Event Loop Blocking: The Silent API Killer and How to Fix It; Node.js Streams Backpressure: The Memory Leak That Kills Servers at 3AM.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.