Vulnerability
Express

SSRF in Express.js Applications

Express.js applications frequently act as intermediaries — proxying requests, fetching remote resources, delivering webhooks, and generating previews. When these features accept user-supplied URLs without validation, attackers exploit them to reach internal services, cloud metadata APIs, and private networks from the server's trusted position.

Scan Your Express App

How SSRF Manifests in Express

SSRF in Express commonly appears in proxy endpoints where the server fetches a URL on behalf of the client. Patterns like app.get('/proxy', (req, res) => fetch(req.query.url)) are direct SSRF vectors. Webhook delivery is another major surface. When users register webhook URLs and the Express server sends HTTP requests to those URLs, attackers register internal addresses to probe the network. File import features that accept URLs (e.g., "import from URL" for CSV files, images, or documents) fetch user-provided URLs server-side. PDF generation services that render HTML from a URL are also commonly exploited. SSRF through HTTP client libraries (axios, node-fetch, got) can be harder to detect because the URL construction may be spread across multiple functions, with user input flowing through several transformations before reaching the fetch call.

Real-World Impact

An Express-based SaaS application offered a "fetch RSS feed" feature where users provided feed URLs. An attacker submitted http://169.254.169.254/latest/meta-data/iam/security-credentials/ and received AWS IAM credentials in the response, which they used to access the application's S3 buckets containing customer data. Another Express API had a /screenshot endpoint that used Puppeteer to render URLs. An attacker provided file:///etc/passwd as the URL, and the headless browser returned the server's password file. They then targeted internal admin panels accessible only from the server's network.

Step-by-Step Fix

1

Build a safe HTTP client wrapper

Create a wrapper around your HTTP client that validates URLs and blocks internal addresses before every request.

// lib/safe-fetch.ts
import dns from 'dns/promises';
import { isIP } from 'net';

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

function isPrivateIP(ip: string): boolean {
  return PRIVATE_RANGES.some(range => range.test(ip));
}

export async function safeFetch(url: string, options?: RequestInit): Promise<Response> {
  const parsed = new URL(url);
  if (![ 'http:', 'https:'].includes(parsed.protocol)) {
    throw new Error(`Blocked protocol: ${parsed.protocol}`);
  }
  const ips = isIP(parsed.hostname)
    ? [parsed.hostname]
    : await dns.resolve4(parsed.hostname);
  if (ips.some(isPrivateIP)) {
    throw new Error('Requests to private IP addresses are blocked');
  }
  return fetch(url, {
    ...options,
    signal: AbortSignal.timeout(10000),
    redirect: 'manual', // Handle redirects manually
  });
}
2

Secure proxy and fetching routes

Replace direct fetch calls in Express routes with the safe wrapper.

import express from 'express';
import { safeFetch } from './lib/safe-fetch';

const app = express();

// UNSAFE
app.get('/proxy', async (req, res) => {
  const response = await fetch(req.query.url as string);
  res.send(await response.text());
});

// SAFE
app.get('/proxy', async (req, res) => {
  try {
    const response = await safeFetch(req.query.url as string);
    const body = await response.text();
    res.set('Content-Type', response.headers.get('content-type') || 'text/plain');
    res.send(body);
  } catch (err) {
    res.status(400).json({ error: 'Invalid or blocked URL' });
  }
});
3

Validate webhook URLs at registration time

Check webhook URLs when users register them, not just when delivering payloads.

import { safeFetch } from './lib/safe-fetch';

app.post('/webhooks', async (req, res) => {
  const { url } = req.body;
  try {
    // Validate URL format and network target
    const parsed = new URL(url);
    if (parsed.protocol !== 'https:') {
      return res.status(400).json({ error: 'Webhooks must use HTTPS' });
    }
    // Test with a HEAD request
    await safeFetch(url, { method: 'HEAD' });
    await db.webhooks.create({ url, userId: req.user.id });
    res.json({ success: true });
  } catch {
    res.status(400).json({ error: 'Invalid webhook URL' });
  }
});
4

Handle redirects safely

When following redirects, re-validate each redirect target to prevent redirect-based SSRF bypass.

async function safeFetchWithRedirects(
  url: string,
  maxRedirects = 3
): Promise<Response> {
  let currentUrl = url;
  for (let i = 0; i <= maxRedirects; i++) {
    const response = await safeFetch(currentUrl, { redirect: 'manual' });
    if ([301, 302, 307, 308].includes(response.status)) {
      const location = response.headers.get('location');
      if (!location) throw new Error('Redirect without Location header');
      currentUrl = new URL(location, currentUrl).toString();
      continue; // safeFetch will re-validate the new URL
    }
    return response;
  }
  throw new Error('Too many redirects');
}

Prevention Best Practices

1. Validate all user-supplied URLs against an allowlist of permitted domains and protocols. 2. Resolve DNS and verify the IP address is not in a private range before making requests. 3. Block file://, gopher://, dict://, and other non-HTTP protocols. 4. Disable HTTP redirects or re-validate the target after each redirect. 5. Use network-level controls (firewall rules, VPC isolation) to limit what the Express server can reach. 6. Set strict timeouts and response size limits on outbound requests.

How to Test

1. Find all endpoints that accept URLs as input (query params, body fields, headers) and submit http://169.254.169.254/latest/meta-data/. 2. Test with http://127.0.0.1:<port> for each internal service port. 3. Try file:///etc/passwd and gopher:// protocol URLs. 4. Test redirect-based bypass: set up a server that 302-redirects to http://169.254.169.254/ and submit its URL. 5. Use Vibe App Scanner to automatically detect SSRF patterns in your Express application.

Frequently Asked Questions

Is URL validation enough to prevent SSRF in Express?

URL validation alone is not sufficient because of DNS rebinding attacks. A domain can resolve to a public IP during validation but then resolve to 127.0.0.1 when the actual request is made. You need to resolve DNS, verify the IP is not private, and then make the request to the verified IP address. Also validate after following any redirects.

How does SSRF work through HTTP redirects?

An attacker hosts a server that responds with a 302 redirect to http://169.254.169.254/. The Express server validates the initial URL (which appears safe), follows the redirect, and fetches the internal address. Prevent this by setting redirect to manual and re-validating each redirect target, or by disabling redirects entirely.

Can SSRF be exploited through axios or node-fetch?

Yes. Any HTTP client library (axios, node-fetch, got, undici) can be used to make SSRF requests. The library itself does not validate URLs against internal addresses. You must add SSRF protection regardless of which HTTP client you use.

Is Your Express App Vulnerable to SSRF?

VAS automatically scans for ssrf vulnerabilities in Express applications and provides step-by-step remediation guidance with code examples.

Scans from $5, results in minutes. Get actionable fixes tailored to your Express stack.