Back to Blog
critical SEVERITY9 min read

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

A critical Server-Side Request Forgery (SSRF) vulnerability was discovered in `functions/stream/createProxyResponse.js`, where the `location` parameter was passed directly to `fetch()` without any URL validation. This allowed attackers to weaponize the proxy function to reach internal network resources, cloud metadata endpoints, and arbitrary external services. The fix adds protocol validation using the `URL` constructor before any fetch operation is performed.

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

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in JavaScript's `createProxyResponse` function, where an unvalidated `location` parameter was passed directly to `fetch()`, enabling attackers to redirect server-side HTTP requests to internal network resources or cloud metadata endpoints. The fix uses the `URL` constructor to parse the incoming location and rejects any request whose protocol is not `http:` or `https:`, blocking non-HTTP schemes like `file://`, `gopher://`, or `dict://` before the fetch is ever attempted.

Vulnerability at a Glance

cweCWE-918
fixParse `location` with `new URL()` and reject any protocol that is not `http:` or `https:` before calling `fetch()`
riskAttackers can force the server to fetch arbitrary internal or external URLs, exposing cloud metadata, internal APIs, and sensitive infrastructure
languageJavaScript
root causeThe `location` parameter in `createProxyResponse()` was passed directly to `fetch()` with no URL parsing, protocol filtering, or allowlist check
vulnerabilityServer-Side Request Forgery (SSRF)

The Vulnerability at a Glance

Field Detail
Vulnerability Server-Side Request Forgery (SSRF)
CWE CWE-918
Language JavaScript
Risk Server fetches attacker-controlled URLs, exposing internal infrastructure
Root Cause location passed directly to fetch() with no URL validation
Fix Protocol check via new URL() before any fetch() call

Quick Answer

What is this vulnerability and how was it fixed?
This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in JavaScript. The createProxyResponse function in functions/stream/createProxyResponse.js accepted a location parameter and passed it directly to fetch() without parsing or validating the URL. An attacker could supply a URL like file:///etc/passwd or http://169.254.169.254/latest/meta-data/ to make the server fetch sensitive internal resources. The fix adds a new URL() parse step and rejects any protocol that is not http: or https:, blocking all non-web schemes before the fetch is ever executed.


Introduction

The functions/stream/createProxyResponse.js file acts as a streaming proxy — it accepts a location parameter and a request object, then uses the server-side fetch() API to retrieve that location on behalf of a client. It is exactly the kind of utility that feels simple and low-risk, right up until an attacker realizes the server will fetch anything you tell it to.

The vulnerability lives in a single line:

const response = await fetch(location, {
  method: request.method,
  headers: request.headers,

No parsing. No protocol check. No hostname validation. Whatever string arrives in location goes straight into fetch(). Combined with a wildcard Access-Control-Allow-Origin: * CORS header that permits any website to trigger cross-origin requests, this function became a fully open proxy that attackers could point at internal infrastructure.


The Vulnerability Explained

What the vulnerable code looked like

Before the fix, the entire createProxyResponse function began like this:

// VULNERABLE — before the fix
export async function createProxyResponnse (location, request) {
    const response = await fetch(location, {
      method: request.method,
      headers: request.headers,
    });
    // ...
}

The location variable is entirely attacker-controlled. There is no call to new URL(), no regular expression check, no allowlist lookup — nothing stands between the raw string and the server-side fetch() call.

Why fetch() makes this dangerous

In a browser, fetch() is constrained by the Same-Origin Policy and CORS. On a server (Node.js, Cloudflare Workers, Deno, etc.), fetch() has no such restrictions. It will happily:

  • Follow http:// and https:// URLs to any host, including localhost and RFC-1918 private ranges
  • Resolve file:// URIs and read local files (in some runtimes)
  • Send requests to gopher://, dict://, and other exotic schemes that can be abused to interact with non-HTTP services
  • Reach cloud provider metadata endpoints that are only accessible from within the cloud environment

A concrete attack scenario

Consider an attacker who discovers this proxy endpoint. They craft a request with:

location = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

The server calls:

await fetch("http://169.254.169.254/latest/meta-data/iam/security-credentials/", ...)

On AWS, this is the Instance Metadata Service (IMDS). The response contains temporary IAM credentials — AccessKeyId, SecretAccessKey, and SessionToken — that grant the attacker the same AWS permissions as the application itself. The wildcard CORS header means a malicious webpage can trigger this fetch from any visitor's browser session, exfiltrating the credentials to an attacker-controlled server.

The same technique works against:

Target URL
GCP metadata http://metadata.google.internal/computeMetadata/v1/
Azure IMDS http://169.254.169.254/metadata/instance
Local Redis http://127.0.0.1:6379/
Internal admin panel http://10.0.0.1/admin
Local file read file:///etc/passwd

Why the wildcard CORS header amplifies the risk

The Access-Control-Allow-Origin: * header means any website — a phishing page, a malicious ad, a compromised third-party script — can make a cross-origin request to this proxy endpoint from inside a victim's browser. The browser will happily attach any cookies or credentials the victim holds, and the proxy will forward the response back to the attacker's page. This turns a server-side vulnerability into a client-assisted attack vector.


The Fix

What changed

The fix adds four lines at the top of createProxyResponnse, before fetch() is ever called:

// FIXED — after the patch
export async function createProxyResponnse (location, request) {
    const parsedUrl = new URL(location);
    if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
        return new Response("Invalid URL", { status: 400 });
    }
    const response = await fetch(location, {
      method: request.method,
      headers: request.headers,
    });
    // ...
}

Before vs. after

 export async function createProxyResponnse (location, request) {
+    const parsedUrl = new URL(location);
+    if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") {
+        return new Response("Invalid URL", { status: 400 });
+    }
     const response = await fetch(location, {
       method: request.method,
       headers: request.headers,

Why each line matters

const parsedUrl = new URL(location);
The URL constructor performs strict RFC-3986 parsing. If location is not a valid URL at all, this throws a TypeError, which should be caught by the surrounding error handler. More importantly, it gives us a structured parsedUrl object with a reliable .protocol property — no regex needed.

if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:")
This is the core gate. It explicitly allows only the two standard web protocols and rejects everything else: file:, gopher:, dict:, ftp:, data:, and any other scheme an attacker might try. Note that URL.protocol always includes the trailing colon (e.g., "https:"), which is why the comparison strings include it.

return new Response("Invalid URL", { status: 400 });
Rather than throwing an unhandled error, the function returns a clean 400 Bad Request response. This is important for proxy functions that may be wrapped in streaming logic — a thrown exception could cause the response stream to hang or leak partial data.

What the fix does NOT yet address

The protocol check is a necessary first step, but a robust SSRF defense also needs:

  • Hostname allowlisting: Even with http:/https: enforced, the server can still reach http://127.0.0.1, http://10.0.0.1, or http://169.254.169.254. A hostname or IP-range blocklist (or better, an explicit allowlist) would close this remaining gap.
  • DNS rebinding protection: Resolve the hostname once and validate the resolved IP before connecting.
  • Redirect following controls: fetch() follows redirects by default. An attacker can use an allowed external URL that redirects to an internal address.

Prevention & Best Practices

1. Always parse before you fetch

Never pass a raw string to fetch(), axios.get(), http.request(), or any HTTP client. Parse it first:

const parsedUrl = new URL(userSuppliedLocation);
// Now validate parsedUrl.protocol, parsedUrl.hostname, etc.

2. Use an explicit allowlist, not just a blocklist

Blocklists are fragile. An allowlist of permitted hostnames or domains is far harder to bypass:

const ALLOWED_HOSTS = new Set(["api.example.com", "cdn.example.com"]);

const parsedUrl = new URL(location);
if (!ALLOWED_HOSTS.has(parsedUrl.hostname)) {
    return new Response("Host not allowed", { status: 400 });
}

3. Block private IP ranges after DNS resolution

If an allowlist is not feasible, resolve the hostname and reject RFC-1918 and link-local addresses:

import dns from "node:dns/promises";

const { address } = await dns.lookup(parsedUrl.hostname);
if (isPrivateIP(address)) {
    return new Response("Internal addresses not allowed", { status: 400 });
}

4. Restrict CORS headers

The wildcard Access-Control-Allow-Origin: * on a proxy endpoint is dangerous. Restrict it to known origins:

const ALLOWED_ORIGINS = ["https://app.example.com"];
const origin = request.headers.get("Origin");
if (ALLOWED_ORIGINS.includes(origin)) {
    headers.set("Access-Control-Allow-Origin", origin);
}

5. Relevant standards and references

  • OWASP SSRF Prevention Cheat Sheet: The definitive guide to layered SSRF defenses
  • CWE-918: Server-Side Request Forgery — the formal classification for this vulnerability class
  • OWASP Top 10 2021 — A10: Server-Side Request Forgery: SSRF earned its own Top 10 category in 2021, reflecting how common and impactful it has become

Key Takeaways

  • createProxyResponse functions are high-value SSRF targets: Any function whose job is to fetch a caller-supplied URL on behalf of a client deserves the strictest URL validation in your codebase.
  • Protocol validation with new URL() is the minimum viable fix: Checking parsedUrl.protocol against an allowlist of "http:" and "https:" blocks file://, gopher://, and other dangerous schemes without breaking legitimate use cases.
  • A wildcard CORS header on a proxy endpoint is a force multiplier: It allows any website to trigger the proxy from a victim's browser, turning a server-side bug into a cross-origin exfiltration primitive.
  • Protocol filtering alone does not stop SSRF to internal HTTP services: After enforcing the protocol, validate the resolved IP address against private ranges to prevent requests to 169.254.169.254, 10.x.x.x, and 127.0.0.1.
  • Cloud metadata endpoints are the most critical SSRF target: A successful fetch to http://169.254.169.254/latest/meta-data/iam/security-credentials/ can yield live AWS IAM credentials, giving an attacker full control of cloud resources.

How Orbis AppSec Detected This

  • Source: The location parameter passed to createProxyResponnse() in functions/stream/createProxyResponse.js — an externally supplied, user-controlled string with no prior sanitization.
  • Sink: fetch(location, { method: request.method, headers: request.headers }) at the top of the function body — a direct, unconditional HTTP request using the tainted value.
  • Missing control: No URL parsing, no protocol allowlist, no hostname validation, and no IP-range blocklist between the source and the sink.
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF).
  • Fix: Added new URL(location) parsing and a protocol guard that returns 400 Bad Request for any scheme other than http: or https: before fetch() is invoked.

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

The createProxyResponse vulnerability is a textbook example of how a single missing validation step can transform a useful utility into a critical security liability. The function's entire purpose — fetching a remote resource on behalf of a caller — is exactly what makes SSRF so dangerous here: the attacker is not subverting the function, they are using it exactly as designed, just pointed at the wrong target.

The fix is elegant in its simplicity: two lines that parse the URL and check the protocol. But the deeper lesson is architectural. Any time your server accepts a URL from an external caller and makes an outbound HTTP request with it, you need to treat that URL as untrusted input and apply the same rigor you would to SQL parameters or shell arguments. Parse it, validate it, and constrain it to only the destinations your application actually needs to reach.

SSRF earned its place in the OWASP Top 10 in 2021 because it is both common and catastrophic. Proxy functions, webhook handlers, URL preview generators, and import-from-URL features are all potential SSRF entry points. Audit yours.


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 endpoints, or loopback addresses, by supplying a malicious URL to a server-side function.

How do you prevent SSRF in JavaScript?

Parse all incoming URLs with `new URL()`, enforce an allowlist of permitted protocols (e.g., only `http:` and `https:`), validate hostnames against a blocklist or allowlist, and never pass raw user-supplied strings directly to `fetch()` or `http.request()`.

What CWE is Server-Side Request Forgery?

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

Is blocking private IP ranges enough to prevent SSRF?

No. IP blocklists can be bypassed through DNS rebinding, decimal/hex IP encoding, and redirect chains. Protocol validation (blocking non-HTTP schemes) and strict hostname allowlists provide stronger defense in depth.

Can static analysis detect SSRF?

Yes. Tools like Semgrep, CodeQL, and Orbis AppSec can trace tainted data from user-controlled inputs to dangerous sinks like `fetch()`, `axios.get()`, or `http.request()` and flag missing URL validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

How Unvalidated Update URLs Happen in Node.js Agent Updaters and How to Fix Them

A critical vulnerability in `agent/src/updater.js` allowed an attacker who could modify the agent's configuration to redirect software update downloads to an attacker-controlled server, enabling remote code execution via a crafted tarball. The fix introduces strict hostname validation — including private network awareness — so the updater only fetches from trusted origins. This kind of supply-chain attack vector is easy to overlook but catastrophic in production agent deployments.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity vulnerability (CVE-2026-69192) was discovered in the ip-address library version 10.1.0, where inconsistent IP address parsing could lead to Server-Side Request Forgery (SSRF) and trust-boundary bypass attacks. The vulnerability was fixed by upgrading ip-address from 10.1.0 to 10.3.1 in the gateway-workflow-dispatcher-v2.js component, preventing attackers from bypassing IP validation checks and accessing internal resources.

critical

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.

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.

critical

How eval() Code Injection happens in JavaScript and how to fix it

A critical code injection vulnerability was discovered in `js/lib/jsencrypt.js` at line 195, where a direct `eval()` call executed a JavaScript string shim for the `process` object in browser environments. If an attacker could influence the string passed to `eval()`—through a compromised dependency, a man-in-the-middle attack, or supply chain tampering—they could achieve arbitrary JavaScript execution in any user's browser. The fix replaces the `eval()` call with the equivalent inline JavaScript