Overview
SSRF occurs when a Node.js Express app accepts a URL input from a client and uses it to perform a server-side HTTP request. In real-world deployments, such patterns appear in APIs that fetch remote resources, download images, or proxy traffic. If the URL input is untrusted, an attacker may steer the server toward internal services, cloud metadata endpoints, or other protected targets, enabling data exposure, unauthorized access, or lateral movement.
In Node.js with Express, SSRF commonly manifests when routes take a query parameter or JSON field that is then passed directly to an HTTP client or a proxy. Typical vulnerable patterns include image fetchers, file download endpoints, or URL-based proxies. The risk is exacerbated by not validating the protocol, host, or network path and by following redirects or allowing arbitrary DNS resolution, which can reach internal addresses and sensitive services.
Defenses include strict input validation, network controls, and safe design choices. Implement an allowlist of permitted hosts or resources, validate the URL with the WHATWG URL constructor, restrict to http/https, and prevent redirects. Consider resolving the hostname and rejecting private IP ranges. Use a dedicated proxy or service wrapper that enforces allowlists, timeouts, and maximum response sizes. Add comprehensive logging and anomaly detection to catch SSRF attempts early.
Code Fix Example
Node.js (Express) API Security Remediation
/* Vulnerable */
const express = require('express');
const app = express();
app.get('/fetch', async (req, res) => {
const url = req.query.url;
const r = await fetch(url);
res.send(await r.text());
});
/* Fixed */
const allowedHosts = new Set(['example.com','api.example.org']);
app.get('/fetch', async (req, res) => {
const url = req.query.url;
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error('Unsupported protocol');
if (!allowedHosts.has(parsed.host)) throw new Error('Host not allowed');
const r = await fetch(url);
res.send(await r.text());
} catch (e) {
res.status(400).send({ error: e.message });
}
});
// Ensure to run with Node.js v18+ for global fetch or include a polyfill if using older versions