Introduction
In the playground.html file, we discovered a critical SSRF vulnerability in the __forEachRdfMessageChunkFromUrl function at line 1843. This function handles streaming RDF message data from external URLs, but a flaw in how it processed the sourceUrl parameter created a serious security risk that could allow attackers to access internal network resources.
The vulnerable code accepted any URL provided by users and passed it directly to the fetch() API with redirect: 'follow' enabled—a dangerous combination that opened the door to Server-Side Request Forgery attacks targeting internal services, cloud metadata endpoints, and administrative interfaces.
The Vulnerability Explained
Server-Side Request Forgery (SSRF) occurs when an application fetches resources from user-supplied URLs without properly validating the destination. In this case, the vulnerable code looked like this:
async function __forEachRdfMessageChunkFromUrl(url, onMessage) {
const sourceUrl = String(url || "").trim();
if (!sourceUrl) throw new Error("--stream-messages needs an RDF Message Log URL...");
const res = await fetch(sourceUrl, { cache: "no-store", referrerPolicy: "no-referrer", redirect: "follow" });
// ...
}
Why This Code Was Dangerous
The function had two critical flaws:
- No URL validation: The
sourceUrlparameter was only checked for emptiness, not for dangerous destinations - Redirect following enabled: The
redirect: 'follow'option meant that even if initial URL validation existed, attackers could bypass it using HTTP redirects
Attack Scenario
An attacker controlling the sourceUrl parameter (via URL parameters, user input, or the __EYELING_URL variable) could exploit this vulnerability in several ways:
Scenario 1: AWS Metadata Access
sourceUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
This would retrieve AWS IAM credentials from the instance metadata service, potentially compromising the entire cloud infrastructure.
Scenario 2: Internal Service Scanning
sourceUrl = "http://10.0.0.1/admin"
sourceUrl = "http://192.168.1.1/api/users"
Attackers could scan and access internal services that are not exposed to the public internet.
Scenario 3: Redirect Bypass
Even with basic URL validation, an attacker could host a redirect on their server:
sourceUrl = "https://attacker.com/redirect"
// Server responds with: HTTP 302 Location: http://169.254.169.254/...
The redirect: 'follow' option would automatically follow this redirect to the internal target.
The Fix
The fix introduces a comprehensive URL validation function called __isBlockedFetchUrl that checks URLs against multiple blocklists before any fetch occurs:
Before (Vulnerable)
async function __forEachRdfMessageChunkFromUrl(url, onMessage) {
const sourceUrl = String(url || "").trim();
if (!sourceUrl) throw new Error("--stream-messages needs an RDF Message Log URL...");
const res = await fetch(sourceUrl, { cache: "no-store", referrerPolicy: "no-referrer", redirect: "follow" });
After (Fixed)
function __isBlockedFetchUrl(u) {
let parsed;
try { parsed = new URL(u, location.href); } catch { return true; }
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return true;
const host = parsed.hostname.toLowerCase();
if (!host || host === "localhost" || host.endsWith(".localhost")) return true;
if (/^(?:127\.|10\.|192\.168\.|169\.254\.|0\.)/.test(host)) return true;
if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(host)) return true;
if (host === "::1" || /^f[cd][0-9a-f]{0,2}:/i.test(host) || /^fe80:/i.test(host)) return true;
return false;
}
async function __forEachRdfMessageChunkFromUrl(url, onMessage) {
const sourceUrl = String(url || "").trim();
if (!sourceUrl) throw new Error("--stream-messages needs an RDF Message Log URL...");
if (__isBlockedFetchUrl(sourceUrl)) throw new Error("--stream-messages URL is not allowed: only public http(s) URLs are permitted, not private/internal/loopback addresses.");
const res = await fetch(sourceUrl, { cache: "no-store", referrerPolicy: "no-referrer", redirect: "error" });
What the Fix Does
The __isBlockedFetchUrl function implements defense-in-depth by checking:
| Check | Purpose |
|---|---|
protocol !== "http:" && protocol !== "https:" |
Blocks file://, javascript:, data: URLs |
host === "localhost" |
Blocks localhost access |
/^(?:127\.\|10\.\|192\.168\.\|169\.254\.\|0\.)/ |
Blocks loopback, Class A private, Class C private, link-local, and null ranges |
/^172\.(1[6-9]\|2\d\|3[0-1])\./ |
Blocks Class B private range (172.16.0.0 - 172.31.255.255) |
host === "::1" and IPv6 patterns |
Blocks IPv6 loopback and private addresses |
Additionally, changing redirect: "follow" to redirect: "error" ensures that any redirect attempt will throw an error rather than being followed, preventing redirect-based bypass attacks.
Prevention & Best Practices
1. Always Validate URLs Before Fetching
Never pass user-controlled URLs directly to fetch(), axios, or similar HTTP clients without validation:
// Good: Validate before fetching
if (isAllowedUrl(userUrl)) {
const response = await fetch(userUrl);
}
// Bad: Direct fetch without validation
const response = await fetch(userUrl);
2. Use Allowlists Over Blocklists When Possible
While blocklists are necessary for IP ranges, consider allowlisting specific domains if your use case permits:
const ALLOWED_DOMAINS = ['api.example.com', 'cdn.example.com'];
function isAllowedDomain(url) {
const parsed = new URL(url);
return ALLOWED_DOMAINS.includes(parsed.hostname);
}
3. Disable Automatic Redirects
Always use redirect: 'error' or redirect: 'manual' when fetching user-controlled URLs:
const response = await fetch(url, { redirect: 'error' });
4. Consider DNS Rebinding Attacks
Advanced attackers may use DNS rebinding to bypass hostname-based validation. For high-security applications, resolve the hostname and validate the IP address before making the request.
Key Takeaways
- The
__forEachRdfMessageChunkFromUrlfunction in playground.html was vulnerable because it fetched user-controlled URLs without any destination validation - The
redirect: 'follow'option created a bypass vector even if basic URL validation had been present - Comprehensive IP range blocking must cover IPv4 private ranges (10.x, 172.16-31.x, 192.168.x), loopback (127.x), link-local (169.254.x), and IPv6 equivalents
- Always disable automatic redirect following when handling user-supplied URLs to prevent redirect-based SSRF bypasses
- URL validation should happen immediately after receiving user input, before any processing or fetch operations
How Orbis AppSec Detected This
- Source: User-controlled URL from the
sourceUrlparameter (derived from URL parameters or__EYELING_URL) - Sink:
fetch(sourceUrl, { redirect: 'follow' })inplayground.html:1843 - Missing control: No validation of URL destination against private IP ranges or internal network addresses
- CWE: CWE-918 (Server-Side Request Forgery)
- Fix: Added
__isBlockedFetchUrl()validation function that blocks private IP ranges, localhost, and link-local addresses, and changed redirect policy from 'follow' to 'error'
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
This SSRF vulnerability in playground.html demonstrates how easily attackers can abuse unvalidated URL handling to access internal resources. The combination of accepting arbitrary URLs and following redirects created a significant security risk that could have led to cloud credential theft, internal service compromise, or data exfiltration.
The fix shows that proper SSRF protection requires multiple layers: protocol validation, hostname blocklisting for private ranges, and disabling automatic redirect following. By implementing these controls in the __isBlockedFetchUrl function, the application now safely handles user-provided URLs while maintaining its core functionality.
When building applications that fetch external resources, always assume user input is malicious and validate thoroughly before making any network requests.