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, 2026•Reviewed 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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3559

Related Articles

high

boardcards.js parseArgs() SSRF: --board Flag Reaches Metadata IPs

The `parseArgs()` function in boardcards.js accepted a user-controlled `--board` command-line argument and passed it directly to fetch() calls without validating the URL scheme or hostname. An attacker could supply `http://169.254.169.254/latest/meta-data/` or other internal addresses to extract cloud credentials or scan internal networks.

high

fetch() with Automatic Redirects Leaks HTTPS Trust to api.tomys.top

The `withProxy()` fetch wrapper allowed automatic HTTP redirects from `api.tomys.top`, trusting the external API's domain integrity without validating final destinations. A compromised or malicious redirect response could send users to attacker-controlled phishing sites while maintaining HTTPS protocol and hostname checks that falsely appear secure.

high

Meta.get() Honored @uploadURL From Script Headers: SSRF Fix

A userscript manager's metadata parser accepted the `@uploadURL` directive from a script's own header block, so any installed or auto-updated script could silently redirect the options-page `uploadScript()` fetch to an attacker-chosen destination — including loopback, link-local metadata endpoints, and LAN addresses. The fix makes `Meta.get()` ignore `@uploadURL` when it appears in a script header, leaving the User Metadata field (validated in `Meta.getUserMeta()`) as the only way to set it, and

high

Unvalidated thumbnailUrl Passed to axios.get(): SSRF Risk

A server-side image helper passes a `thumbnailUrl` value — sourced from 3D-print file metadata returned by an OctoPrint instance — straight into `axios.get()` with no scheme, host, or IP validation. Anyone who can write that metadata (a crafted G-code file, or a compromised/spoofed OctoPrint endpoint) turns the application into an HTTP proxy for internal networks and cloud metadata services. The accompanying pull request bumps `react-router-dom` from 7.9.1 to 7.18.4 to pick up the CVE-2026-21884

high

esearch() SSRF: requests.get() Trusted Any Host in the URL

A citation format-conversion script used by an AI research skill built HTTP URLs from user-supplied PMIDs, DOIs, arXiv IDs, and free-text queries, then passed the resulting string straight to `requests.get()` with no check that it still pointed at an intended API host. The fix introduces an `ALLOWED_HOSTS` set containing the three real upstream APIs and an `_is_allowed_url()` helper that compares `urlparse(url).hostname` against it before the request is issued. This closes a CWE-918 server-side

high

updateCardBg() Follows Unvalidated 302 Location Headers

A background-image updater fetched a configured image URL with manual redirect handling and then re-issued the request to whatever `Location` header came back, with no scheme or host checks. A redirect to `http://169.254.169.254/` or `http://127.0.0.1:<port>/` would have been followed with the original fetch options attached, and the response body written to disk as an image asset. The fix resolves the redirect target against `imgDownloadUrl` and rejects anything that is not HTTPS on the same ho