flawopen.com/Server-Side Request Forgery/JavaScript

Server-Side Request Forgery in JavaScript

High Severity CWE-918
ELI5

You give a delivery bot a delivery address. The bot is allowed inside the building. If the user tells the bot 'deliver to 127.0.0.1:6379', the bot walks straight to your Redis database and speaks raw commands.

Key terms on this page
Lookup Callback
The 'lookup' option in Node.js http.Agent that overrides DNS resolution before opening the TCP socket.
Private Subnet
IPv4 address ranges reserved by RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and link-local (169.254.0.0/16).

What's happening

In Node.js, libraries like axios, node-fetch, and native fetch resolve hostnames and connect without inspecting the destination IP. Attackers use this to query internal Kubernetes pods, Redis instances, or cloud metadata.

Real-world impact

In 2023, numerous webhooks and PDF generation microservices in Node.js allowed attackers to access local Docker daemon sockets (http://localhost:2375) and achieve full host container escape.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: Naively fetching user URL with axios
const express = require('express');
const axios = require('axios');

const app = express();

app.get('/proxy-image', async (req, res) => {
  const imageUrl = req.query.url;
  // Attacker input: http://169.254.169.254/latest/meta-data/
  try {
    const response = await axios.get(imageUrl);
    res.send(response.data);
  } catch (err) {
    res.status(500).send('Fetch error');
  }
});
FIXED
// HARDENED: Custom DNS lookup agent blocking private & metadata IP ranges
const express = require('express');
const http = require('http');
const https = require('https');
const dns = require('dns');
const ipaddr = require('ipaddr.js'); // npm i ipaddr.js
const axios = require('axios');

const app = express();

function safeLookup(hostname, options, callback) {
  dns.lookup(hostname, options, (err, address, family) => {
    if (err) return callback(err);

    try {
      const addr = ipaddr.parse(address);
      const range = addr.range();

      // Deny private, loopback, link-local, and reserved ranges
      const blockedRanges = ['loopback', 'private', 'linkLocal', 'carrierGradeNat', 'reserved'];
      if (blockedRanges.includes(range)) {
        return callback(new Error(`SSRF Blocked: Destination IP ${address} is in blocked range (${range})`));
      }

      callback(null, address, family);
    } catch (parseErr) {
      callback(new Error('Invalid IP address resolved'));
    }
  });
}

const safeHttpAgent = new http.Agent({ lookup: safeLookup });
const safeHttpsAgent = new https.Agent({ lookup: safeLookup });

app.get('/proxy-image', async (req, res) => {
  const imageUrl = req.query.url;
  try {
    const parsed = new URL(imageUrl);
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return res.status(400).send('Invalid protocol');
    }

    const response = await axios.get(imageUrl, {
      httpAgent: safeHttpAgent,
      httpsAgent: safeHttpsAgent,
      maxRedirects: 0, // Prevent redirect bypasses
      timeout: 3000
    });

    res.send(response.data);
  } catch (err) {
    res.status(403).send('Request blocked: ' + err.message);
  }
});

Why the fix works

The custom lookup function in Node's HTTP/HTTPS Agent intercepts DNS resolution at the socket level. Before the TCP handshake begins, ipaddr.js evaluates the resolved IP. If it falls into loopback, private, or link-local ranges, the socket is aborted immediately, completely preventing DNS rebinding.

Gotchas

Native fetch does not support Agent lookup

Global fetch() in Node.js 18+ uses undici, which handles DNS differently. Use an explicit Dispatcher or a validated library like axios with custom agent.

Common misconceptions

"Checking URL hostname is sufficient"

Hostnames like 'localtest.me' or 'customer.com' can point to 127.0.0.1 or 169.254.169.254. DNS resolution inspection is mandatory.

How to check if you're affected

npm audit npx eslint --plugin security

Prevention checklist

FAQ

Can IPv6 bypass Node.js SSRF filters?

Yes, if IPv6 is not handled. Addresses like ::1, ::ffff:127.0.0.1, or fc00::/7 are private. ipaddr.js handles both IPv4 and IPv6.

References