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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #11

Related Articles

medium

How gitlab.bandit.B501 happens in Python and how to fix it

The `proverbia-scraper.py` script disabled TLS certificate verification on its `requests.get()` call and silenced the resulting security warnings, exposing the scraper to man-in-the-middle attacks. The fix removes the `verify=False` flag and the warning suppression, restoring proper certificate validation while keeping the existing 30-second timeout intact.

high

How Server-Side Request Forgery (SSRF) happens in Go HTTP handlers and how to fix it

A Server-Side Request Forgery (SSRF) vulnerability was discovered in `internal/web/controller/server.go` where the `applySubTemplate` endpoint accepted arbitrary URLs from user input and passed them directly to `serverService.ApplySubTemplateFromGithub()` without any host validation. An attacker could exploit this to make the server issue HTTP requests to internal network resources, cloud metadata endpoints, or redirect-controlled destinations. The fix introduces a strict allowlist that restrict

critical

How SSRF via Vulnerable Dependency Versions Happens in Node.js and How to Fix It

A permissive semver range in `package.json` allowed npm to install axios versions vulnerable to SSRF (CVE-2024-39338). By bumping the minimum version from `^1.6.0` to `^1.7.4`, all downstream consumers of this SDK are now protected from server-side request forgery attacks. This critical fix required changing just one line in the dependency manifest.

critical

How Server-Side Request Forgery happens in Python FastAPI and how to fix it

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in app.py where the `/parse` and `/parse-video` endpoints accepted user-supplied URLs with only substring validation. The application checked if 'doubao.com' appeared anywhere in the URL string, allowing attackers to bypass this check and access internal services, cloud metadata endpoints, or scan the internal network. The fix implemented proper hostname parsing with an allowlist of legitimate domains.

critical

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `maintenance/getImages.js`, where the `getImage()` function passed database-sourced URLs directly to `axios.get()` without any validation. An attacker who could modify the elements database could redirect these requests to internal network resources — including AWS cloud metadata endpoints — potentially exposing IAM credentials and other sensitive infrastructure data. The fix introduces a strict URL allowlist that limi

high

How SSRF via inconsistent IP address parsing happens in Node.js dependencies and how to fix it

A high-severity flaw (CVE-2026-69192) in the widely-used `ip-address` npm package meant that IP strings could be parsed inconsistently compared to the OS resolver and Node's own networking stack — letting an attacker slip a private/loopback address past an allowlist that used `Address4`/`Address6` for validation. This PR pins and upgrades `ip-address` from `10.1.0` to `10.3.1` in both `package.json` (via `overrides`) and `package-lock.json`, eliminating the parser divergence across the whole dep