SSRF

SSRF in Node.js Express guide [Jun 2026] [GHSA-88gm-j2wx-58h6]

[Updated Jun 2026] Updated GHSA-88gm-j2wx-58h6

Overview

SSRF vulnerabilities in Node.js Express apps arise when the server proxies or fetches content from a URL supplied by a client. Attackers can leverage this to reach internal services, such as databases, private APIs, or cloud instance metadata, potentially exfiltrating data or performing further attacks from the server's network position. In Express, SSRF commonly manifests when routes accept a user-provided URL and perform HTTP requests, image proxying, or resource fetching without destination validation. Adversaries can use this to access non-public endpoints, bypass network controls, or perform port scans from the host. Common patterns include proxy routes, image fetchers, or webhooks that call out to arbitrary URLs based on user input. Attackers may attempt to access services inside a VPC, sensitive management interfaces, or the cloud metadata service by supplying crafted URLs. Defensive coding and proper architecture are essential. No CVE IDs are provided in this prompt. This guide describes general SSRF risk and Node.js Express-specific mitigations, focusing on input validation, allowlisting, timeouts, and safe proxy practices.

Code Fix Example

Node.js (Express) API Security Remediation
/* Vulnerable and fixed example in one snippet (side by side) */
const express = require('express');
const fetch = require('node-fetch'); // npm i node-fetch@2

const app = express();

// ------------------ Vulnerable pattern ------------------
// This endpoint proxies a user-supplied URL without validation
app.get('/proxy', async (req, res) => {
  const url = req.query.url;
  const response = await fetch(url);
  const data = await response.buffer();
  res.set('Content-Type', response.headers.get('content-type') || 'application/octet-stream');
  res.send(data);
});

// ------------------ Fixed pattern ------------------
const ALLOWED_HOSTS = ['example.com', 'images.example.com'];

function isAllowedUrl(u) {
  try {
    const parsed = new URL(u);
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false;
    const host = parsed.hostname;
    return ALLOWED_HOSTS.includes(host);
  } catch {
    return false;
  }
}

app.get('/proxy-secure', async (req, res) => {
  const url = req.query.url;
  if (!url || !isAllowedUrl(url)) {
    return res.status(400).send('Invalid URL');
  }
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);
    const response = await fetch(url, { signal: controller.signal });
    clearTimeout(timeout);
    const data = await response.buffer();
    res.set('Content-Type', response.headers.get('content-type') || 'application/octet-stream');
    res.send(data);
  } catch {
    res.status(502).send('Failed to fetch URL');
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log('Server listening on port ' + PORT));

CVE References

Choose which optional cookies to allow. You can change this any time.