Back to Blog
critical SEVERITY5 min read

How Server-Side Request Forgery (SSRF) happens in Node.js and how to fix it

A critical SSRF vulnerability was discovered in `fetch-worker.js` where URLs from `sources.txt` were fetched without any validation, allowing attackers to target internal services and cloud metadata endpoints. The fix implements a robust URL allowlist that enforces HTTPS and blocks requests to private IP ranges, localhost, and link-local addresses.

O
By Orbis AppSec
Published August 24, 2026Reviewed August 24, 2026

Answer Summary

Server-Side Request Forgery (SSRF) in Node.js (CWE-918) occurs when an application fetches URLs from untrusted sources without validation. In this case, `fetch-worker.js` read URLs from `sources.txt` and fetched them directly, allowing attackers to access internal services. The fix adds an `isAllowedUrl()` function that validates the protocol is HTTPS and blocks private IP ranges (127.x, 10.x, 192.168.x, 172.16-31.x, 169.254.x) before any fetch occurs.

Vulnerability at a Glance

cweCWE-918
fixAdded isAllowedUrl() function enforcing HTTPS and blocking private IP ranges
riskAccess to internal services, cloud metadata, and sensitive endpoints
languageJavaScript (Node.js)
root causeURLs from sources.txt fetched without domain or IP validation
vulnerabilityServer-Side Request Forgery (SSRF)

Introduction

The fetch-worker.js file handles background URL fetching using Node.js worker threads, processing URLs from a sources.txt file. However, a critical flaw in the message handler at line 4 created a severe security risk: URLs were passed directly to fetch operations without any validation of the target domain or IP address.

This meant that if an attacker could modify sources.txt—through a separate file write vulnerability, supply chain attack, or compromised configuration—they could force the application to make requests to internal services, cloud metadata endpoints like http://169.254.169.254/, or other sensitive network resources that should never be accessible from the outside.

The vulnerable code pattern was deceptively simple:

parentPort.on('message', async (message) => {
    const { id, url, userAgent } = message;
    // url is used directly without any validation
    // ... fetch operation proceeds
});

The Vulnerability Explained

Server-Side Request Forgery (SSRF) occurs when an application can be tricked into making HTTP requests to unintended destinations. In this case, the fetch-worker.js worker thread accepted any URL passed through the parentPort.on('message') handler and would attempt to fetch it without question.

The Dangerous Code Path

Looking at the original code:

const { parentPort } = require('worker_threads');

parentPort.on('message', async (message) => {
    const { id, url, userAgent } = message;
    // No validation whatsoever - any URL is accepted
    let headerUA = userAgent || 'unknown';
    // ... proceeds to fetch the URL
});

The url variable extracted from the message could contain:
- Internal IP addresses: http://192.168.1.1/admin
- Localhost services: http://127.0.0.1:8080/internal-api
- Cloud metadata endpoints: http://169.254.169.254/latest/meta-data/
- Internal Kubernetes services: http://kubernetes.default.svc/api/v1/secrets

Attack Scenario

Consider this attack against a Jellyfin server deployment:

  1. An attacker identifies that sources.txt is used to configure external data sources
  2. Through a configuration vulnerability or social engineering, they add a malicious entry: http://169.254.169.254/latest/meta-data/iam/security-credentials/
  3. The fetch worker processes this URL and retrieves AWS IAM credentials
  4. The response containing sensitive credentials is returned through the worker's message system
  5. The attacker now has temporary AWS credentials with whatever permissions the instance role provides

This is particularly dangerous because:
- The request originates from within the trusted network perimeter
- Cloud metadata services don't require authentication from the instance
- The application appears to be functioning normally while exfiltrating sensitive data

The Fix

The fix introduces a new isAllowedUrl() function that implements a strict allowlist approach, validating URLs before any fetch operation occurs.

Before (Vulnerable)

parentPort.on('message', async (message) => {
    const { id, url, userAgent } = message;
    // Immediately proceeds to use the URL
    let headerUA = userAgent || 'unknown';

After (Secure)

function isAllowedUrl(url) {
    let parsed;
    try { parsed = new URL(url); } catch { return false; }
    if (parsed.protocol !== 'https:') return false;
    const host = parsed.hostname;
    if (/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|0\.0\.0\.0)/i.test(host)) return false;
    return true;
}

parentPort.on('message', async (message) => {
    const { id, url, userAgent } = message;

    if (!isAllowedUrl(url)) {
        parentPort.postMessage({ id, success: false, error: 'URL not allowed', url });
        return;
    }
    // Only now proceeds with the fetch

How Each Change Protects Against SSRF

  1. URL Parsing with try/catch: Malformed URLs that could bypass string-based checks are rejected immediately
  2. HTTPS-only enforcement: parsed.protocol !== 'https:' blocks HTTP, file://, gopher://, and other dangerous protocols
  3. Private IP blocking: The regex pattern blocks:
    - localhost - the loopback hostname
    - 127. - IPv4 loopback range
    - 10. - Class A private network
    - 192.168. - Class C private network
    - 172.16-31. - Class B private networks
    - 169.254. - Link-local and cloud metadata range
    - 0.0.0.0 - All interfaces binding address

  4. Early return with error: Invalid URLs trigger an immediate response with success: false and a clear error message, preventing any network request

Prevention & Best Practices

1. Always Validate URLs Before Fetching

// Good: Validate before use
function safeFetch(url) {
    const parsed = new URL(url);
    if (!isAllowedDestination(parsed)) {
        throw new Error('URL not permitted');
    }
    return fetch(url);
}

2. Use Allowlists, Not Blocklists

While the fix uses a blocklist for private IPs (which is appropriate for this use case), consider maintaining an explicit allowlist of permitted domains when possible:

const ALLOWED_DOMAINS = ['api.example.com', 'cdn.trusted.org'];

function isDomainAllowed(hostname) {
    return ALLOWED_DOMAINS.some(domain => 
        hostname === domain || hostname.endsWith('.' + domain)
    );
}

3. Resolve DNS and Validate the IP

Sophisticated attackers may use DNS rebinding. For high-security applications:

const dns = require('dns').promises;

async function isIPAllowed(hostname) {
    const addresses = await dns.resolve4(hostname);
    return addresses.every(ip => !isPrivateIP(ip));
}

4. Network Segmentation

Deploy applications that fetch external URLs in isolated network segments where they cannot reach internal services, even if SSRF occurs.

Security Standards Reference

  • OWASP: SSRF is listed in the OWASP Top 10 2021 as A10
  • CWE-918: Server-Side Request Forgery

Key Takeaways

  • Never fetch URLs from sources.txt or similar configuration files without validation—the fetch-worker.js pattern of reading and fetching is common but dangerous
  • The 169.254.x.x range is critical to block—this is where cloud metadata services live on AWS, GCP, and Azure
  • HTTPS enforcement provides defense in depth—it prevents protocol smuggling attacks and ensures encrypted transport
  • Worker threads don't provide security isolation—the parentPort.on('message') handler needs the same input validation as any other entry point
  • Regex-based IP blocking must cover all private ranges—missing even one range (like 172.16-31.x) leaves a gap attackers will find

How Orbis AppSec Detected This

  • Source: URLs read from sources.txt and passed via parentPort.on('message') in fetch-worker.js
  • Sink: The fetch operation that would make HTTP requests to attacker-controlled destinations
  • Missing control: No validation of URL protocol, hostname, or IP address before fetching
  • CWE: CWE-918 (Server-Side Request Forgery)
  • Fix: Added isAllowedUrl() function that enforces HTTPS protocol and blocks private IP ranges before any fetch operation

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This SSRF vulnerability in fetch-worker.js demonstrates how a simple oversight—trusting URLs from a configuration file—can create a critical security risk. The fix is elegant and focused: a 14-line isAllowedUrl() function that validates protocol and blocks private networks before any fetch occurs.

For Node.js developers building applications that fetch external resources, remember: every URL from an external source is potentially malicious. Validate the protocol, check the destination, and when in doubt, use an explicit allowlist of permitted domains.

References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can make a server-side application send HTTP requests to arbitrary destinations, potentially accessing internal services, cloud metadata endpoints, or bypassing firewalls.

How do you prevent SSRF in Node.js?

Validate all URLs before fetching by checking the protocol (allow only HTTPS), blocking private IP ranges and localhost, using allowlists for permitted domains, and parsing URLs safely with the URL constructor.

What CWE is SSRF?

SSRF is classified as CWE-918: Server-Side Request Forgery.

Is blocking localhost enough to prevent SSRF?

No, blocking localhost alone is insufficient. Attackers can use alternative representations like 127.0.0.1, 0.0.0.0, IPv6 addresses, or DNS rebinding to bypass simple localhost checks. You must block all private IP ranges.

Can static analysis detect SSRF?

Yes, static analysis tools can detect SSRF by tracing data flow from untrusted sources (like file reads) to HTTP fetch calls. Tools like Semgrep, CodeQL, and specialized SAST scanners can identify missing URL validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #34

Related Articles

high

How Inconsistent IP Address Parsing Happens in JavaScript and How to Fix It

A high-severity vulnerability in the `ip-address` npm package (CVE-2026-69192) allowed attackers to craft IPv4 addresses with leading-zero octets that the library decoded as decimal while system resolvers decoded them as octal — creating a dangerous parsing discrepancy that could enable Server-Side Request Forgery (SSRF) and trust-boundary bypass. The fix upgrades `ip-address` from version 10.1.0 to 10.3.1 in the `core/http/react-ui` frontend dependency tree, eliminating the inconsistency and en

critical

How Server-Side Request Forgery (SSRF) Happens in Node.js and How to Fix It

A critical Server-Side Request Forgery (SSRF) vulnerability in `src/fetch.js` allowed the `fetchPage()` function to access internal network addresses, private IP ranges, and cloud metadata endpoints without any validation. This fix hardens input validation to block requests to RFC 1918 private addresses, localhost, and cloud metadata endpoints, preventing attackers from exploiting the function to probe internal infrastructure.

high

How SSRF via IP Address Parsing Inconsistency happens in Node.js and how to fix it

A critical parsing inconsistency in the ip-address npm package (versions before 10.3.1) allowed Server-Side Request Forgery (SSRF) and trust-boundary bypass. The library decoded IP addresses with leading-zero octets as decimal (e.g., 0127.0.0.1 as 127.0.0.1), while DNS resolvers and system libraries interpreted them as octal (e.g., 0127 as 87 decimal), enabling attackers to bypass IP allowlists and access internal resources.

high

How Octal IP Address Parsing Inconsistency Enables SSRF in Node.js and How to Fix It

A critical parsing inconsistency in the `ip-address` npm package (version 10.2.0) allowed attackers to bypass SSRF protections by exploiting how leading-zero octets are interpreted differently—decimal by the library versus octal by system resolvers. This vulnerability (CVE-2026-69192) was fixed by upgrading to version 10.3.1 using an npm override, ensuring consistent IP address validation across the application.

high

How NO_PROXY bypass via crafted URL happens in Node.js axios and how to fix it

A high-severity vulnerability (CVE-2026-42043) in the axios HTTP client library allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This could route sensitive internal traffic through attacker-controlled proxy servers. The fix upgrades axios from 1.13.6 to 1.18.0, which includes a rewritten proxy resolution mechanism using `proxy-from-env` v2.1.0 and the `https-proxy-agent` package.

critical

How Denial of Service via Gzip Bomb happens in Node.js and how to fix it

CVE-2026-59873 is a critical Denial of Service vulnerability in node-tar versions prior to 7.5.19, where a maliciously crafted gzip bomb can exhaust server resources when extracting archives. The fix upgrades the `tar` dependency from version 7.5.15 to 7.5.21 in `package-lock.json` and pins the version via an `overrides` block in `package.json`. Any application that processes user-supplied tar archives is at risk of resource exhaustion, making this an urgent upgrade.