Express.js Server-Side Request Forgery (SSRF)

High Risk Server-Side Request Forgery
expressssrfjavascriptserver-sidehttp-requestsweb-security

What it is

The Express.js application allows user-controlled input to influence server-side HTTP requests, potentially enabling attackers to access internal services, cloud metadata, or other restricted resources. This vulnerability occurs when user input is directly used in HTTP requests without proper validation and sanitization.

// Vulnerable: Direct use of user input in request
app.get('/fetch', (req, res) => {
  const url = req.query.url;
  axios.get(url).then(response => {
    res.json(response.data);
  });
});
// Secure: URL validation and allowlisting
const ALLOWED_DOMAINS = ['api.example.com', 'safe-service.com'];

app.get('/fetch', (req, res) => {
  const url = req.query.url;
  try {
    const parsedUrl = new URL(url);
    if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) {
      return res.status(400).json({ error: 'Invalid domain' });
    }
    axios.get(url).then(response => {
      res.json(response.data);
    });
  } catch (error) {
    res.status(400).json({ error: 'Invalid URL' });
  }
});

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Express applications construct server-side HTTP requests using user-controlled input directly as the URL without validation: axios.get(req.query.url) or fetch(req.body.imageUrl). Attackers provide URLs pointing to internal services (http://localhost:6379/), cloud metadata endpoints (http://169.254.169.254/latest/meta-data/), or internal infrastructure (http://internal-db:5432/). This enables access to services protected by firewall rules that allow connections from the application server but not from the internet, bypassing network-based access controls.

Root causes

User Input Directly Used in HTTP Request URLs

Express applications construct server-side HTTP requests using user-controlled input directly as the URL without validation: axios.get(req.query.url) or fetch(req.body.imageUrl). Attackers provide URLs pointing to internal services (http://localhost:6379/), cloud metadata endpoints (http://169.254.169.254/latest/meta-data/), or internal infrastructure (http://internal-db:5432/). This enables access to services protected by firewall rules that allow connections from the application server but not from the internet, bypassing network-based access controls.

Insufficient Validation of User-Provided URLs

Applications attempt URL validation using incomplete or bypassable checks like verifying the URL starts with 'http://' or checking for specific domains using string matching rather than proper URL parsing. Attackers bypass weak validation using URL encoding (%68%74%74%70://internal), alternative URL schemes (file://, gopher://), redirect chains to internal resources, DNS rebinding attacks, or @ symbols to bypass hostname checks (https://evil.com@169.254.169.254/). Insufficient validation fails to prevent access to internal IP ranges (127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 169.254.0.0/16).

Missing Domain and IP Range Allowlists

Applications lack explicit allowlists defining which external domains or IP ranges are permitted for outbound requests. Without allowlists, applications make requests to any user-specified destination. Even applications that blocklist known dangerous destinations (localhost, 127.0.0.1, metadata endpoints) fail to block all internal IP ranges, IPv6 loopback (::1), domain variants (localtest.me resolves to 127.0.0.1), or newly discovered internal endpoints. Allowlist-based validation is more secure but often omitted due to perceived inconvenience or incomplete understanding of SSRF risks.

Improper URL Parsing and Validation

Applications use string manipulation or regular expressions instead of proper URL parsing libraries to validate user-provided URLs. Code performs checks like url.includes('localhost') or url.startsWith('http://') which are easily bypassed. Developers fail to use Node.js URL class or url.parse() to properly extract and validate scheme, hostname, port, and path components. Inconsistent URL parsing between validation code and HTTP client library leads to validation bypasses where the validator and client interpret URLs differently, allowing attackers to craft URLs that pass validation but access restricted resources.

Direct User Input in HTTP Client Libraries

Express applications use HTTP client libraries (axios, node-fetch, request, got) with user input passed directly to request methods without intermediate validation: axios.get(userUrl), fetch(userProvidedEndpoint), or request(req.params.webhookUrl). Applications implement features like webhook testing, URL preview generation, image proxying, feed aggregation, or remote data fetching without SSRF protection. Developers trust that HTTP libraries will handle URLs safely, unaware that these libraries faithfully execute requests to any reachable destination including internal infrastructure.

Fixes

1

Implement Strict URL Validation with Domain Allowlisting

Create explicit allowlists of permitted external domains and validate all user-provided URLs against this list before making requests. Use Node.js URL class to parse URLs and extract hostname: const parsedUrl = new URL(userInput); if (!allowedDomains.includes(parsedUrl.hostname)) throw Error. Define allowlists per feature: webhooks might allow specific webhook.site domains, image proxies might allow cdn.example.com. Validate protocol is http or https only, reject file://, gopher://, ftp://. Check resolved IP addresses against internal ranges before making requests. Document allowlist maintenance procedures for adding new permitted domains.

2

Use URL Parsing Libraries for Comprehensive Validation

Always use Node.js URL class or url.parse() for URL validation instead of string manipulation or regex. Parse user input with try/catch to handle malformed URLs: try { const url = new URL(userInput); } catch { return error; }. Validate each URL component separately: url.protocol must be http: or https:, url.hostname must not be IP addresses in private ranges, url.port must be standard ports (80, 443) unless specifically allowed. Check for authentication info in URL (url.username, url.password) which attackers use for SSRF exploitation. Normalize URLs before comparison to prevent bypass through URL encoding or alternative representations.

3

Restrict Outbound Requests to Known Safe Destinations

Configure HTTP client libraries with strict defaults that reject requests to internal resources. Use axios with custom baseURL limiting requests to approved domains or implement request interceptors that validate URLs before execution: axios.interceptors.request.use(config => { validateUrl(config.url); return config; }). For webhook features, implement two-tier validation: pre-flight validation of destination, then actual request with timeout and size limits. Create separate HTTP client instances for external API calls vs internal service communication to prevent confusion between trusted and untrusted destinations.

4

Implement Network-Level Controls to Block Private IP Ranges

Deploy network-layer protections alongside application-level validation. Configure egress firewall rules blocking outbound connections to private IP ranges: 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, and IPv6 equivalents (::1/128, fc00::/7). Use DNS resolver that blocks resolution of internal hostnames or implement custom DNS resolver that validates resolved IPs before allowing connections. Deploy application in network segmentation where application servers cannot directly access internal infrastructure. Use AWS VPC endpoints, private link, or service mesh for legitimate internal service communication.

5

Sanitize and Pre-Validate All User Input for Requests

Implement multi-layer validation before using user input in HTTP requests. Validate URL format and structure, check length limits (reject URLs > 2048 chars), verify TLD against known public TLDs to prevent typosquatting, resolve hostname to IP and check against private range blocklists before making requests. Implement rate limiting on outbound request features to prevent SSRF-based scanning of internal networks. Log all outbound request destinations for security monitoring and anomaly detection. Use Content Security Policy and CORS headers to limit client-side request capabilities. Implement request signing or authentication for legitimate use cases to prevent request tampering.

6

Use Indirect Object References Instead of Direct URLs

Replace user-provided URLs with indirect object references where possible. Instead of accepting arbitrary URLs for webhook callbacks, have users register webhook endpoints which receive IDs: POST /webhooks -> returns webhookId; reference as /webhooks/{webhookId} in callbacks. For image proxying, pre-fetch and cache allowed images, then serve by ID rather than proxying arbitrary URLs. For RSS feeds, maintain internal registry of approved feeds with IDs. For API integrations, use OAuth flows where user authorizes access rather than providing credentials or URLs. This architectural change eliminates SSRF attack surface by removing direct URL input entirely.

Detect This Vulnerability in Your Code

Sourcery automatically identifies express.js server-side request forgery (ssrf) and many other security issues in your codebase.