Skip to content

Node.js SSRF Protection — Complete Guide to Server-Side Request Forgery Prevention

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Node.js SSRF Protection. We cover key concepts, practical examples, and best practices to help you master this topic.

Server-Side Request Forgery (SSRF) attacks trick Node.js servers into making requests to internal resources, bypassing firewalls to access cloud metadata, databases, and internal services.

What You'll Learn

By the end of this tutorial, you'll implement SSRF protection through URL validation, IP allowlisting, DNS rebinding prevention, request timeouts, and secure HTTP client configuration.

Why SSRF Matters

SSRF is a critical OWASP vulnerability that exposes internal infrastructure. Cloud metadata endpoints at 169.254.169.254 are common targets. A single SSRF can leak cloud provider credentials.

Real-World Use

A URL preview service extracts Open Graph data from user-submitted URLs. Without SSRF protection, an attacker submits http://169.254.169.254/latest/meta-data/ to steal AWS credentials.

SSRF Protection Path

flowchart LR
  A[Security Checklist] --> B[SSRF Protection]
  B --> C[Helmet/CORS]
  B --> D[JWT/OAuth]
  C --> E[Deployment]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

URL Validation

Validate and sanitize URLs before making requests. Block internal and private IP ranges.

const { URL } = require("node:url");
function isValidUrl(urlString) {
  try {
    const parsed = new URL(urlString);
    if (!["http:", "https:"].includes(parsed.protocol)) return false;
    if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") return false;
    if (parsed.hostname.endsWith(".local") || parsed.hostname.endsWith(".internal")) return false;
    return true;
  } catch {
    return false;
  }
}
console.log(isValidUrl("https://example.com/data")); // true
console.log(isValidUrl("http://localhost:3000/admin")); // false
console.log(isValidUrl("http://169.254.169.254/")); // false

IP Allowlisting

Resolve hostnames and check against an allowlist of permitted IP ranges.

const dns = require("node:dns");
const { netmask } = require("netmask");
const ALLOWED_RANGES = [
  new netmask("93.184.216.0/24"), // example.com
  new netmask("140.82.112.0/20"), // github.com
];
async function isAllowedHost(hostname) {
  return new Promise((resolve) => {
    dns.resolve4(hostname, (err, addresses) => {
      if (err) return resolve(false);
      const allowed = addresses.some((ip) =>
        ALLOWED_RANGES.some((range) => range.contains(ip))
      );
      resolve(allowed);
    });
  });
}

DNS Rebinding Protection

Prevent DNS rebinding attacks by validating IPs both before and after connection.

const dns = require("node:dns");
const http = require("node:http");
async function safeFetch(url) {
  const parsed = new URL(url);
  const initialIps = await dns.promises.resolve4(parsed.hostname);
  if (initialIps.some((ip) => isPrivateIP(ip))) {
    throw new Error("Blocked private IP");
  }
  return new Promise((resolve, reject) => {
    const req = http.get(url, { lookup: (host, opts, cb) => {
      dns.lookup(host, { family: 4 }, (err, ip) => {
        if (err) return cb(err);
        if (isPrivateIP(ip)) return cb(new Error("Blocked DNS rebinding"));
        cb(null, ip, 4);
      });
    }}, resolve);
    req.on("error", reject);
    req.setTimeout(5000, () => { req.destroy(); reject(new Error("Timeout")); });
  });
}
function isPrivateIP(ip) {
  return /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(ip);
}

Request Timeouts

Always set timeouts on outgoing requests to prevent hanging connections.

const http = require("node:http");
function fetchWithTimeout(url, timeoutMs = 5000) {
  return new Promise((resolve, reject) => {
    const req = http.get(url, (res) => {
      let data = "";
      res.on("data", (chunk) => data += chunk);
      res.on("end", () => resolve(data));
    });
    req.setTimeout(timeoutMs, () => {
      req.destroy();
      reject(new Error("Request timeout"));
    });
    req.on("error", reject);
  });
}

Using Blocked IP Libraries

Use established libraries like ssrf-req-filter or ip-range-check for production SSRF protection.

const http = require("node:http");
const { BlockList } = require("node:net");
const blocklist = new BlockList();
blocklist.addAddress("127.0.0.1");
blocklist.addRange("10.0.0.0", "10.255.255.255");
blocklist.addRange("172.16.0.0", "172.31.255.255");
blocklist.addSubnet("192.168.0.0", 16);
blocklist.addSubnet("169.254.0.0", 16);
function isBlocked(ip) {
  return blocklist.check(ip);
}

Common Mistakes

1. Only Checking URL in Frontend

Frontend validation is bypassed easily. Always validate URLs on the server side.

2. Not Resolving DNS at Request Time

DNS rebinding changes IP resolution between validation and request. Resolve at request time.

3. Allowing file:// or gopher:// Protocols

SSRF Attacks use non-HTTP protocols. Only allow http and https.

4. No Timeout on Outbound Requests

Attackers can make your server hang indefinitely. Always set timeouts.

5. Trusting URL Parsing for Validation

URLs can contain obfuscated IPs, unicode domains, or redirect chains. Validate after normalization.

Practice Questions

1. What is SSRF?

Server-Side Request Forgery: tricking a server into making requests to internal or restricted resources.

2. Why is 169.254.169.254 a common SSRF target?

It is the cloud metadata endpoint for AWS, GCP, and Azure, containing instance credentials.

3. What is DNS rebinding?

An attack where a domain resolves to a benign IP first, then switches to a malicious IP after validation.

4. How do you prevent DNS rebinding?

Validate the IP both before the connection and at connection time, rejecting private IPs.

5. Challenge: Implement a URL fetch function with complete SSRF protection.

async function fetchUrl(url, options = {}) {
  const parsed = new URL(url);
  if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid protocol");
  if (isPrivateHost(parsed.hostname)) throw new Error("Blocked private host");
  const { lookup } = require("node:dns").promises;
  const [ip] = await lookup(parsed.hostname, { family: 4 });
  if (isPrivateIP(ip)) throw new Error("Blocked private IP");
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), options.timeout || 5000);
  const response = await fetch(url, { signal: controller.signal });
  clearTimeout(timeout);
  return response;
}

FAQ

What is the difference between SSRF and CSRF?

SSRF tricks the server into making requests (server-side). CSRF tricks the user's browser into making requests (client-side).

Can SSRF be exploited through redirects?

Yes. The initial URL may be safe, but a redirect leads to an internal resource. Follow redirects with validation.

How does AWS IMDSv1 vs v2 affect SSRF?

IMDSv1 is vulnerable to SSRF. IMDSv2 requires a token header, providing partial protection.

Should I block all private IP ranges?

Yes, for outbound HTTP requests from public-facing services. Internal services need different rules.

What is the best SSRF protection library?

ssrf-req-filter and ip-range-check are popular. The BlockList class in Node.js net module is also effective.

Mini Project: SSRF-Protected URL Fetcher

Build a URL fetcher middleware with comprehensive SSRF protection.

const { BlockList } = require("node:net");
const dns = require("node:dns");
const blocklist = new BlockList();
[["127.0.0.0", 8], ["10.0.0.0", 8], ["172.16.0.0", 12], ["192.168.0.0", 16], ["169.254.0.0", 16]]
  .forEach(([addr, subnet]) => blocklist.addSubnet(addr, subnet));
const http = require("node:http");
class SafeFetcher {
  async get(url, timeout = 5000) {
    const parsed = new URL(url);
    if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("Unsupported protocol");
    const [ip] = await dns.promises.resolve4(parsed.hostname);
    if (blocklist.check(ip)) throw new Error("Blocked IP");
    return new Promise((resolve, reject) => {
      const req = http.get(url, { lookup: (h, o, cb) => cb(null, ip, 4) }, resolve);
      req.setTimeout(timeout, () => { req.destroy(); reject(new Error("Timeout")); });
      req.on("error", reject);
    });
  }
}

What's Next

Node.js GraphQL Node.js WebSocket Node.js JWT Authentication

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro