Back to Blog
critical SEVERITY7 min read

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

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `server.js` and `worker.js`, where user-supplied `config` URL parameters were passed directly to `fetchWithAuth()` without any validation. This allowed attackers to force the application to make requests to internal network addresses, cloud metadata endpoints like `169.254.169.254`, or `file://` URIs. The fix introduces an `isAllowedUrl()` allowlist function that rejects private IP ranges, loopback addresses, and non-HTTP(S) pr

O
By Orbis AppSec
Published July 27, 2026Reviewed July 27, 2026

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in Node.js, affecting `server.js` and `worker.js`. The `?config=` URL query parameter was passed directly to `fetchWithAuth()` without validation, enabling attackers to target internal services, cloud metadata endpoints (e.g., `http://169.254.169.254/`), or `file://` URIs. The fix adds an `isAllowedUrl()` function that enforces protocol allowlisting (HTTP/HTTPS only) and blocks private IP ranges (`10.x`, `172.16–31.x`, `192.168.x`, `169.254.x`) and loopback addresses before any request is dispatched.

Vulnerability at a Glance

cweCWE-918
fixAdded `isAllowedUrl()` function that enforces HTTP/HTTPS-only protocols and blocks all RFC-1918 private IP ranges and link-local addresses
riskAttackers can exfiltrate cloud credentials, probe internal services, or access local files via the server's network identity
languageJavaScript (Node.js)
root causeThe `configUrl` query parameter was passed directly to `fetchWithAuth()` in both `server.js` and `worker.js` without protocol or IP range validation
vulnerabilityServer-Side Request Forgery (SSRF)

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

Introduction

The server.js file in this Node.js application handles remote configuration loading — a common and useful pattern where a client passes a URL pointing to a YAML config file. But a critical flaw in how that URL was consumed created a textbook Server-Side Request Forgery (SSRF) vulnerability: the configUrl parameter was read directly from the query string and handed to fetchWithAuth() with zero validation.

Here's the vulnerable code path in server.js, around line 52:

// BEFORE: No validation — any URL accepted
const configUrl = reqUrl.searchParams.get('config');
if (configUrl) {
  const configRes = await fetchWithAuth(configUrl);  // ← direct sink
  ...
}

And the same pattern existed independently in worker.js:

const configUrl = url.searchParams.get('config');
if (configUrl) {
  const configRes = await fetchWithAuth(configUrl);  // ← same sink, same risk
  ...
}

In both files, an attacker who can reach the /sub endpoint controls exactly where the server makes its next HTTP request. That's SSRF — and in cloud-hosted environments, it's a direct path to credential theft.


The Vulnerability Explained

What's actually happening

When a user hits /sub?config=https://example.com/config.yaml, the application fetches that YAML file and uses it for configuration. This is intentional behavior. The problem is that nothing stops the user from supplying any URL at all — including URLs that target infrastructure that's only reachable from the server's own network.

The fetchWithAuth() function is designed to make authenticated outbound requests. By feeding it an attacker-controlled URL, the attacker hijacks the server's network identity to probe or exfiltrate data from:

  • Cloud metadata serviceshttp://169.254.169.254/latest/meta-data/iam/security-credentials/ (AWS Instance Metadata Service)
  • Internal microserviceshttp://10.0.0.5:8080/admin/dump
  • Loopback serviceshttp://127.0.0.1:6379 (Redis, databases, etc.)
  • Non-HTTP schemesfile:///etc/passwd (if the fetch implementation supports it)

The attack scenario

The PR documents the most dangerous exploit path concisely:

GET /sub?config=http://169.254.169.254/latest/meta-data/iam/security-credentials/

On an AWS EC2 instance or ECS container, this request returns a JSON blob containing temporary AWS credentials — AccessKeyId, SecretAccessKey, and Token — that grant full IAM permissions to whatever role the instance is running as. The attacker receives this response through the application's normal config-fetching response path, with no special access required beyond reaching the /sub endpoint.

The same attack works against GCP's metadata server (http://metadata.google.internal/), Azure's IMDS (http://169.254.169.254/metadata/instance), and any internal service that trusts requests originating from the application server's IP.

Why both files matter

server.js and worker.js implement the same ?config= feature independently. Fixing only one would leave a parallel attack surface open. This is a common pitfall in codebases where logic is duplicated across a main server process and a worker process — each copy of the vulnerable pattern needs its own fix.


The Fix

The fix introduces a dedicated isAllowedUrl() function in both server.js and worker.js that validates any user-supplied URL before it reaches fetchWithAuth().

The new validation function

// AFTER: Added to both server.js and worker.js
function isAllowedUrl(urlStr) {
  const parsed = new URL(urlStr);
  if (!['http:', 'https:'].includes(parsed.protocol)) return false;
  const host = parsed.hostname;
  if (/^(localhost|127\.|0\.0\.0\.0|::1$)/.test(host)) return false;
  if (/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(host)) return false;
  return true;
}

Let's break down what each check does:

Check What it blocks
['http:', 'https:'].includes(parsed.protocol) file://, ftp://, gopher://, data: URIs, and any other non-web scheme
/^(localhost\|127\.\|0\.0\.0\.0\|::1$)/ IPv4 and IPv6 loopback addresses, blocking access to local services
/^(10\.\|172\.(1[6-9]\|2\d\|3[01])\.\|192\.168\.\|169\.254\.)/ RFC-1918 private ranges AND the 169.254.x.x link-local range used by cloud metadata services

Before and after comparison

server.js — before:

if (configUrl) {
  const configRes = await fetchWithAuth(configUrl);
  if (!configRes.ok) throw new Error(`Failed to fetch remote config: ${configRes.status}`);

server.js — after:

if (configUrl) {
  if (!isAllowedUrl(configUrl)) throw new Error('Invalid or disallowed config URL');
  const configRes = await fetchWithAuth(configUrl);
  if (!configRes.ok) throw new Error(`Failed to fetch remote config: ${configRes.status}`);

worker.js — before:

if (configUrl) {
  const configRes = await fetchWithAuth(configUrl);

worker.js — after:

if (configUrl) {
  if (!isAllowedUrl(configUrl)) return new Response('Invalid or disallowed config URL', { status: 400 });
  const configRes = await fetchWithAuth(configUrl);

Notice the slightly different error handling between the two files: server.js throws an error (which the surrounding try/catch handles), while worker.js returns a 400 Bad Request Response directly — matching each file's existing error-handling idiom. This is the right approach: the validation logic is identical, but the failure response respects each module's architecture.

Why new URL() is the right parsing tool

Using JavaScript's built-in URL constructor to parse the input before validation is critical. String-based checks like if (configUrl.startsWith('http')) are trivially bypassed with tricks like:
- http://169.254.169.254@evil.com (credentials in URL)
- URL-encoded characters
- Mixed-case protocols like HTTP:

The URL constructor normalizes all of these before the regex checks run, making the validation robust against encoding tricks.


Prevention & Best Practices

1. Always validate URLs before server-side fetches

Any time user input influences an outbound HTTP request, apply URL validation before the request is made. The check should happen at the point of consumption, not upstream — as demonstrated by placing isAllowedUrl() directly before fetchWithAuth().

2. Use allowlists, not just blocklists

The current fix uses a blocklist of known-dangerous ranges. For higher-security applications, consider inverting this: maintain an explicit allowlist of domains or IP ranges that the application is permitted to fetch from, and reject everything else. This is harder to bypass because unknown future infrastructure is blocked by default.

3. Resolve DNS before validating (DNS rebinding defense)

The current fix validates the hostname string, but a sophisticated attacker can use DNS rebinding: register a domain that initially resolves to a public IP (passing validation), then quickly switch the DNS record to 169.254.169.254 before the actual fetch. To prevent this, resolve the hostname to an IP address first, then validate the resolved IP. Libraries like ssrf-req-filter for Node.js handle this automatically.

4. Apply the fix to every fetch call site

Because server.js and worker.js each independently implemented the ?config= feature, both needed independent fixes. Audit your codebase for every location where user input flows into fetch(), http.request(), axios.get(), or similar — not just the most obvious one.

5. Use a dedicated SSRF-prevention library

For production applications, consider using a purpose-built SSRF prevention library rather than hand-rolled regex. Libraries maintain up-to-date blocklists and handle edge cases (IPv6, URL encoding, CNAME chains) more reliably.

OWASP and CWE references

  • CWE-918: Server-Side Request Forgery
  • OWASP Top 10 2021 — A10: Server-Side Request Forgery
  • OWASP SSRF Prevention Cheat Sheet: Comprehensive guidance on blocklist strategies, allowlists, and network-layer controls

Key Takeaways

  • The ?config= parameter in server.js and worker.js was a direct SSRF sink — any URL accepted by fetchWithAuth() was reachable by any client who could hit the /sub endpoint.
  • The AWS metadata endpoint 169.254.169.254 is the canonical SSRF target in cloud environments; blocking the entire 169.254.x.x range is a mandatory baseline for any application making server-side HTTP requests.
  • Duplicated vulnerable patterns require duplicated fixesworker.js had the same flaw as server.js and needed its own isAllowedUrl() call, not just a shared import.
  • new URL() normalization before regex validation prevents encoding bypasses — always parse before you validate.
  • SSRF is not a theoretical risk in cloud environments — a single GET request to /sub?config=http://169.254.169.254/... could have exfiltrated live AWS IAM credentials.

How Orbis AppSec Detected This

  • Source: HTTP query parameter config (url.searchParams.get('config')) in both server.js and worker.js
  • Sink: fetchWithAuth(configUrl) called with the unvalidated user-supplied URL, in server.js:52 and the corresponding line in worker.js
  • Missing control: No protocol validation, no IP range blocklist, no allowlist — the URL was used as-is with no sanitization between source and sink
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF)
  • Fix: Added isAllowedUrl() in both files to enforce HTTP/HTTPS-only protocols and block all RFC-1918 private ranges and the 169.254.x.x link-local range before any outbound request is dispatched

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

SSRF vulnerabilities in configuration-fetching features are particularly dangerous because they're easy to overlook — the feature is supposed to make outbound HTTP requests, so the behavior looks normal. What makes it a vulnerability is the absence of constraints on where those requests can go. In cloud-hosted applications, the gap between "fetch any URL" and "exfiltrate IAM credentials" is a single crafted GET request.

The fix here is a strong baseline: parse the URL with new URL(), enforce HTTP/HTTPS protocols, and block all private and link-local IP ranges. For applications where the set of valid config sources is known in advance, upgrading to an explicit allowlist would provide even stronger guarantees. Either way, the core principle holds: never let user-supplied URLs reach your HTTP client without validation.


References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can cause a server to make HTTP requests to unintended destinations — including internal services, cloud metadata APIs, or loopback addresses — by supplying a malicious URL as input.

How do you prevent SSRF in Node.js?

Validate all user-supplied URLs before making outbound requests: enforce HTTP/HTTPS-only protocols, block private IP ranges (RFC-1918), loopback addresses, and link-local ranges like 169.254.0.0/16 using an allowlist function before calling fetch() or similar APIs.

What CWE is Server-Side Request Forgery?

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

Is input sanitization alone enough to prevent SSRF in Node.js?

No. String-based sanitization is fragile and bypassable (e.g., via URL encoding, IPv6, or DNS rebinding). You must parse the URL with the `URL` constructor, then validate the resolved protocol and hostname against an explicit blocklist or allowlist of permitted ranges.

Can static analysis detect SSRF?

Yes. Tools like Semgrep, CodeQL, and Orbis AppSec can trace tainted data from HTTP request parameters to dangerous sink functions like `fetch()` or `http.request()`, flagging cases where no URL validation occurs between source and sink.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

critical

How Server-Side Request Forgery (SSRF) happens in JavaScript fetch() and how to fix it

A critical Server-Side Request Forgery vulnerability in `popup.js` allowed attackers to inject malicious URLs from scraped webpages directly into fetch() calls, potentially accessing internal network resources and AWS metadata endpoints. The fix adds URL validation to ensure only HTTP/HTTPS protocols are used and blocks requests to private IP ranges and localhost addresses.

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 axios versions prior to 1.15.1 allowed attackers to bypass NO_PROXY environment variable restrictions using specially crafted URLs. This meant HTTP requests intended to stay internal could be routed through an attacker-controlled proxy, potentially exposing sensitive data. The fix upgrades axios to version 1.15.1, which correctly validates URLs against NO_PROXY rules.

critical

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `hubcmdui/routes/httpProxy.js` where the `/proxy/test` endpoint passed a user-controlled `testUrl` parameter directly to `axios.get()` without any validation. An attacker could exploit this to probe internal infrastructure — including AWS metadata endpoints, Redis instances, and private network ranges. The fix introduces a strict URL validation function that blocks private IP ranges, loopback addresses, and non-HTTP(S)

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) was discovered in axios versions prior to 1.15.1, allowing attackers to bypass NO_PROXY configurations using specially crafted URLs. This could expose sensitive internal traffic to proxy servers, potentially leaking credentials or internal data. The fix upgrades axios to version 1.15.1, which properly validates URLs before applying proxy exclusion rules.

high

How SSRF and Credential Leakage via Absolute URLs happens in axios and how to fix it

A high-severity vulnerability (CVE-2025-27152) in axios versions prior to 1.8.2 allowed Server-Side Request Forgery (SSRF) attacks and credential leakage when making HTTP requests with absolute URLs. This vulnerability was fixed by upgrading from axios 1.7.4 to 1.8.2 in both package.json and package-lock.json, eliminating the attack vector that could have exposed authentication tokens and enabled unauthorized server-side requests.

high

How Server-Side Request Forgery (SSRF) happens in Python Flask and how to fix it

A high-severity Server-Side Request Forgery (SSRF) vulnerability was discovered in `webui/backend/main.py` at line 6597 of the Posterizarr project. User-controlled `request.media_type` was interpolated directly into a URL used for server-side HTTP requests to The Movie Database (TMDB) API, allowing attackers to manipulate the destination of outbound requests. The fix introduces a strict allowlist that only permits `"movie"` or `"tv"` as valid media types.