Back to Blog
critical SEVERITY8 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 `libraries/microworlds/video.js` where the `VideoCanvasWrapper.loadVideo()` function passed user-controlled URLs directly to `fetch()` without any validation. An attacker could exploit this by supplying URLs pointing to internal services, localhost endpoints, or malicious external servers. The fix introduces strict URL parsing and protocol validation before any network request is made.

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

Answer Summary

This is a Server-Side Request Forgery (SSRF) vulnerability (CWE-918) in JavaScript's `VideoCanvasWrapper.loadVideo()` function in `libraries/microworlds/video.js`. The function passed a user-controlled `src` parameter directly to `fetch()` with no URL scheme or domain validation, allowing attackers to probe internal services or exfiltrate data. The fix uses the `URL` constructor to parse and validate the URL, then enforces that only `http:` or `https:` protocols are permitted before any network call is made.

Vulnerability at a Glance

cweCWE-918
fixParse the URL with `new URL()` and enforce `http:` or `https:` protocol before calling `fetch()`
riskAttackers can make the application fetch arbitrary internal or external URLs, exposing internal services and enabling data exfiltration
languageJavaScript
root causeUser-controlled `src` parameter passed directly to `fetch()` in `VideoCanvasWrapper.loadVideo()` without URL validation
vulnerabilityServer-Side Request Forgery (SSRF)

How Server-Side Request Forgery (SSRF) Happens in JavaScript and How to Fix It

The Vulnerability at a Glance

Field Detail
Vulnerability Server-Side Request Forgery (SSRF)
CWE CWE-918
Language JavaScript
Risk Attackers can make the application fetch arbitrary internal or external URLs
Root Cause User-controlled URL passed directly to fetch() without validation
Fix Parse URL with new URL() and enforce http:/https: protocol allowlist

Introduction

The libraries/microworlds/video.js file handles video loading for the Microworlds environment — a seemingly straightforward task of fetching a video resource by URL. But a flaw in VideoCanvasWrapper.loadVideo() at line 213 turned this routine operation into a serious security risk: any URL a user could pass to the vid_costume primitive would be handed directly to fetch() with zero validation.

This is a textbook Server-Side Request Forgery (SSRF) vulnerability. The function trusted the caller completely, meaning an attacker who could influence the src argument could point the application at internal services, cloud metadata endpoints, or attacker-controlled servers — all without any warning or restriction.


The Vulnerability Explained

What the Code Was Doing

Before the fix, VideoCanvasWrapper.loadVideo() looked like this:

VideoCanvasWrapper.prototype.loadVideo = function(src){
    fetch(src).then(function(response) {
        return response;
    }).catch(function() {
        // ...
    });
}

The src parameter flows directly into fetch() — no parsing, no protocol check, no domain restriction. Whatever string arrives as src, the browser or runtime will dutifully attempt to fetch it.

How an Attacker Would Exploit This

The vid_costume primitive in the Microworlds environment is the user-facing entry point that calls loadVideo(). An attacker (or a malicious project shared with another user) could invoke it like this:

vid_costume(sprite, 'http://localhost:8080/admin', false, 'Loading')

This would cause the application to make an HTTP request to the local machine's admin interface on port 8080 — a port that would normally be inaccessible from the outside. The response data could then be exfiltrated or used to probe the internal network topology.

Other concrete attack scenarios include:

  • Cloud metadata exfiltration: http://169.254.169.254/latest/meta-data/iam/security-credentials/ — the AWS instance metadata endpoint, which can expose IAM credentials.
  • Internal service enumeration: Looping through http://192.168.1.1 through http://192.168.1.254 to map internal network services.
  • Data exfiltration to attacker servers: https://evil.example.com/collect?data=... to send internal data outbound.
  • Non-HTTP protocol abuse: file:///etc/passwd or ftp://internal-server/ if the runtime supports those schemes.

Why This Matters for Microworlds

Microworlds projects can be shared between users. A malicious project author could embed a crafted vid_costume call that silently probes the victim's local network or internal services when another user opens the project. The victim would see only a video loading spinner — while their machine makes requests they never intended.


The Fix

What Changed

The fix adds a validation block at the very top of VideoCanvasWrapper.loadVideo(), before fetch() is ever called:

VideoCanvasWrapper.prototype.loadVideo = function(src){
    // NEW: Parse and validate the URL before fetching
    var parsedUrl;
    try {
        parsedUrl = new URL(src);
    } catch(e) {
        throw new Error("Invalid video URL.");
    }
    if(parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
        throw new Error("Video URL must use http or https protocol.");
    }
    // Only reaches fetch() if URL is valid and uses http/https
    fetch(src).then(function(response) {
        return response;
    }).catch(function() {
        // ...
    });
}

Before vs. After

Before (vulnerable):

VideoCanvasWrapper.prototype.loadVideo = function(src){
    fetch(src).then(function(response) {
        return response;
    }).catch(function() { /* ... */ });
}

After (fixed):

VideoCanvasWrapper.prototype.loadVideo = function(src){
    var parsedUrl;
    try {
        parsedUrl = new URL(src);
    } catch(e) {
        throw new Error("Invalid video URL.");
    }
    if(parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
        throw new Error("Video URL must use http or https protocol.");
    }
    fetch(src).then(function(response) {
        return response;
    }).catch(function() { /* ... */ });
}

Why This Fix Works

Step 1 — new URL(src): The built-in URL constructor is the correct tool for parsing URLs. It is not fooled by encoding tricks, null bytes, or malformed strings that might slip past a regex. If src cannot be parsed as a valid URL, it throws a TypeError immediately, which the catch block converts into a clear error message.

Step 2 — Protocol allowlist: After parsing, parsedUrl.protocol is checked against an explicit allowlist of 'https:' and 'http:'. This single check eliminates:
- file:// — local filesystem reads
- ftp:// — FTP requests
- data: — inline data URIs that could bypass other checks
- javascript: — JavaScript execution via URL
- Any other non-standard scheme

The combination of these two steps means fetch() is only ever called with a structurally valid URL that uses a web-standard protocol. Localhost and internal IP addresses are not explicitly blocked by this fix (which would require hostname validation as a further hardening step), but the protocol restriction alone eliminates the most dangerous attack vectors like file:// and custom scheme abuse.


Prevention & Best Practices

1. Always Parse URLs with a Proper Parser

Never use string matching or regex alone to validate URLs. The URL constructor in modern JavaScript is reliable and handles edge cases that manual parsing misses:

// ❌ Fragile — easily bypassed
if (src.startsWith('https://')) { fetch(src); }

// ✅ Correct — uses the URL parser
try {
    const parsed = new URL(src);
    if (parsed.protocol !== 'https:') throw new Error('Protocol not allowed');
    fetch(src);
} catch(e) {
    console.error('Invalid URL:', e.message);
}

2. Consider a Hostname Allowlist for Stricter Control

The current fix blocks dangerous protocols but does not restrict which hostnames can be fetched. For a higher-security environment, add an explicit hostname allowlist:

const ALLOWED_HOSTS = ['cdn.example.com', 'media.example.com'];

if (!ALLOWED_HOSTS.includes(parsedUrl.hostname)) {
    throw new Error("Video URL hostname is not permitted.");
}

3. Block Internal IP Ranges (Defense in Depth)

If hostname allowlisting is not feasible, consider blocking known internal ranges:

const BLOCKED_PATTERNS = [/^localhost$/i, /^127\./, /^10\./, /^192\.168\./, /^169\.254\./];

if (BLOCKED_PATTERNS.some(p => p.test(parsedUrl.hostname))) {
    throw new Error("Video URL must not point to internal addresses.");
}

4. Apply the Same Pattern Everywhere fetch() is Called

Search your codebase for every call to fetch(), XMLHttpRequest, axios.get(), or similar network primitives. Any that accept user-controlled URLs need the same URL validation treatment.

5. Use Static Analysis to Catch This Early

Tools that can detect unvalidated URL usage flowing into fetch():

  • Semgrep: Rules for tainted data flowing to fetch() — see semgrep.dev rules for SSRF
  • CodeQL: JavaScript/TypeScript SSRF query pack
  • Orbis AppSec: Automatically traced the src parameter from vid_costume through to fetch() and flagged the missing validation

6. OWASP Guidance

OWASP classifies SSRF as part of the OWASP Top 10 (A10:2021 – Server-Side Request Forgery) and provides a dedicated SSRF Prevention Cheat Sheet.


Key Takeaways

  • VideoCanvasWrapper.loadVideo() was the exact sink — any SSRF fix in this file must gate the fetch() call, not just log or catch errors after the fact.
  • The vid_costume primitive is a user-facing entry point — user-controlled data reached fetch() through a code path that appeared to be a simple media feature, not an obvious security boundary.
  • new URL() is the right tool for URL validation in JavaScript — string prefix checks like startsWith('https://') are insufficient and bypassable.
  • Protocol allowlisting eliminates entire classes of attack — blocking everything except http: and https: removes file://, ftp://, data:, and custom scheme abuse in a single check.
  • SSRF can hide in media and asset loading code — not just in explicit API proxy endpoints. Any function that calls fetch() with external input is a potential SSRF vector.

How Orbis AppSec Detected This

  • Source: The src parameter of VideoCanvasWrapper.loadVideo(), supplied by user input via the vid_costume Microworlds primitive.
  • Sink: fetch(src) at libraries/microworlds/video.js:213, called with the unvalidated src value.
  • Missing control: No URL parsing, protocol validation, or hostname restriction was present before the fetch() call.
  • CWE: CWE-918 — Server-Side Request Forgery (SSRF).
  • Fix: Added new URL() parsing with a try/catch block and a protocol allowlist check (http: and https: only) 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 SSRF vulnerability in VideoCanvasWrapper.loadVideo() is a reminder that security boundaries exist everywhere user input flows into network calls — not just in obvious places like login forms or API endpoints. A video loading utility in an educational coding environment is just as capable of becoming an SSRF vector as a dedicated HTTP proxy, if the URL it receives is never validated.

The fix is elegant precisely because it is minimal: two validation steps added before the existing fetch() call, with no changes to the happy path for legitimate http:// and https:// video URLs. The URL constructor handles the heavy lifting of parsing, and the protocol check enforces the allowlist. Together, they close the attack surface without breaking any valid use case.

When you write code that calls fetch() — or any other network primitive — with a URL that could be influenced by user input, treat that URL as untrusted data. Parse it. Check its protocol. Consider its hostname. Then fetch.


References

Frequently Asked Questions

What is Server-Side Request Forgery (SSRF)?

SSRF is a vulnerability where an attacker can cause the server or application to make HTTP requests to an arbitrary destination — including internal services, localhost, or cloud metadata endpoints — by supplying a malicious URL as input.

How do you prevent SSRF in JavaScript?

Always parse user-supplied URLs with `new URL()`, enforce an allowlist of permitted protocols (e.g., only `http:` and `https:`), and ideally restrict requests to an allowlist of trusted hostnames or domains.

What CWE is Server-Side Request Forgery?

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

Is input sanitization enough to prevent SSRF in JavaScript?

Sanitization alone is not sufficient. You must validate the URL structure using a proper URL parser like `new URL()` and enforce protocol and hostname restrictions — string-based sanitization is easily bypassed.

Can static analysis detect SSRF vulnerabilities?

Yes. Static analysis tools like Semgrep, CodeQL, and multi-agent AI scanners can trace tainted data flow from user input to dangerous sinks like `fetch()` or `XMLHttpRequest` and flag unvalidated URL usage.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3559

Related Articles

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.

high

How Server-Side Request Forgery (SSRF) happens in Node.js through inconsistent IP address parsing and how to fix it

A high-severity Server-Side Request Forgery (SSRF) vulnerability (CVE-2026-69192) was discovered in the ip-address package version 10.2.0, where inconsistent IP address parsing allowed attackers to bypass trust boundaries and access internal resources. The fix upgrades ip-address from 10.2.0 to 10.3.1 across the dependency tree, with explicit pinning in package.json and strategic version management in bun.lock to prevent both direct and transitive exploitation paths.

high

How IP Address Parsing Inconsistency Happens in Node.js and How to Fix It

CVE-2026-69192 revealed a critical inconsistency in the `ip-address` npm package where the `Address4` class decoded leading-zero octets as decimal while standard DNS resolvers interpreted them as octal, creating a trust-boundary bypass and SSRF attack vector. The fix upgrades `ip-address` from version 10.2.0 to 10.3.1 in the CanvaLight plugin, correcting the parsing behavior to match resolver expectations.

high

How Message-Level Raw Option Bypass happens in Node.js Nodemailer and how to fix it

A high-severity vulnerability in Nodemailer (GHSA-p6gq-j5cr-w38f) allowed attackers to bypass the `disableFileAccess` and `disableUrlAccess` security controls by using the message-level `raw` option, enabling arbitrary file reads and full-response SSRF in delivered emails. The fix upgrades Nodemailer from version 6.10.1 to 9.0.1, closing this bypass at the library level. This is especially critical for applications that allow any user-influenced content to flow into email composition.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.