The Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Server-Side Request Forgery (SSRF) via unvalidated third-party API URLs |
| CWE | CWE-918 |
| Language | JavaScript |
| Risk | Attacker-controlled URLs can redirect user browsers to internal network resources or sensitive local files |
| Root cause | convertToLocalFormat() stored track.url and track.pic from Meting API responses without any protocol or domain validation |
| Fix | Added isSafeUrl() to enforce https: protocol on all stored URLs; tracks with invalid audio URLs are skipped and invalid cover URLs are set to null |
Frequently Asked Questions
What is Server-Side Request Forgery (SSRF)?
SSRF is an attack where an application fetches a URL supplied by an attacker, causing the request to target internal network resources, cloud metadata endpoints, or local files that would otherwise be inaccessible from the outside.
How do you prevent SSRF in JavaScript URL handling?
Parse every externally supplied URL with the native URL constructor, enforce https: protocol, and optionally allowlist trusted domains before storing or fetching any URL.
What CWE is SSRF?
SSRF is classified as CWE-918: Server-Side Request Forgery.
Is checking for http vs https enough to prevent SSRF?
Enforcing HTTPS blocks plaintext and non-HTTP schemes (like file:// or ftp://), which eliminates the most common SSRF vectors. For stronger protection, combine protocol enforcement with an allowlist of trusted domains.
Can static analysis detect SSRF in JavaScript?
Yes. Tools like Semgrep and multi-agent AI scanners can trace tainted data from external API responses to fetch/URL storage sinks and flag missing protocol validation, as demonstrated by the scanner that caught this exact issue.
Introduction
The js/cd-player/playlist-importer.js file is responsible for fetching playlist data from third-party Meting APIs and converting those API responses into the internal track format used by the CD player. That sounds routine — but a flaw in the convertToLocalFormat() function created a meaningful security risk: every URL returned by the Meting API was stored verbatim, with no check on the protocol or origin of the URL.
This matters because those stored URLs — specifically audioUrl and coverUrl — are later fetched directly from the user's browser when they save music locally. If an attacker can influence what the Meting API returns (or operate a malicious Meting-compatible server), they can inject arbitrary URLs, including file:// paths, http:// addresses pointing to internal services, or cloud metadata endpoints like http://169.254.169.254/latest/meta-data/. The application would then faithfully pass those URLs to the browser for fetching, turning the user's own machine into a vehicle for probing internal infrastructure.
The Vulnerability Explained
The Vulnerable Code
Before the fix, convertToLocalFormat() in playlist-importer.js (around line 112) looked like this:
for (const track of tracks) {
// Skip tracks without valid URL
if (!track.url) {
console.warn('Skipping track without URL:', track.name);
continue;
}
// ... (track conversion logic)
audioUrl: track.url,
coverUrl: track.pic, // Store original URL, don't resolve yet
The only guard on track.url was a truthiness check — if the string was non-empty, it passed straight through into audioUrl. The coverUrl field (track.pic) had no check at all. Both values came directly from the Meting API response, which is an external, third-party data source.
Why This Is a Problem
The Meting API is a music metadata aggregation service. The application queries it via fetchPlaylistWithFallback(), which iterates over multiple API servers. Any of those servers could, in theory, be compromised or replaced with an attacker-controlled endpoint. Even without a full server compromise, a man-in-the-middle on an HTTP Meting endpoint could inject malicious URLs into the response.
Once a malicious URL is stored in audioUrl or coverUrl, it waits silently in the track record. When the user later triggers a "save locally" action, the application fetches those URLs from the user's browser context. This is the classic SSRF pattern applied to a client-side application: the browser becomes the unwitting HTTP client, sending requests to wherever the attacker points it.
Concrete Attack Scenario
- An attacker operates a Meting-compatible API server or intercepts traffic to an existing one.
- They craft a playlist response where
track.urlis set tohttp://192.168.1.1/admin(a common router admin panel) andtrack.picis set tofile:///etc/passwd. - The user imports this playlist.
convertToLocalFormat()stores both URLs without complaint. - When the user clicks "Save locally," the browser fetches
http://192.168.1.1/admin, potentially leaking router configuration data, and attempts to loadfile:///etc/passwdas cover art, potentially exposing local file contents.
For users on corporate networks, this attack surface extends to internal APIs, development servers, and cloud metadata services — all reachable from the user's machine but not from the public internet.
The Fix
The New isSafeUrl() Function
The fix introduces a small, focused validation helper added just before convertToLocalFormat():
/**
* Validate that a URL uses HTTPS to prevent SSRF via attacker-controlled URLs
* @param {string} url - URL to validate
* @returns {boolean}
*/
function isSafeUrl(url) {
try {
return new URL(url).protocol === 'https:';
} catch {
return false;
}
}
This function uses the native URL constructor, which is the correct tool for parsing URLs in JavaScript. If the input is not a valid URL (e.g., a relative path, a garbled string, or a javascript: URI), the constructor throws and isSafeUrl returns false. If it is a valid URL but uses any protocol other than https: — including http:, file:, ftp:, data:, or blob: — it also returns false. Only well-formed HTTPS URLs pass.
Before and After
Before (vulnerable):
// Skip tracks without valid URL
if (!track.url) {
console.warn('Skipping track without URL:', track.name);
continue;
}
// ...
audioUrl: track.url,
coverUrl: track.pic, // Store original URL, don't resolve yet
After (fixed):
// Skip tracks without valid HTTPS URL (prevents SSRF via attacker-controlled URLs)
if (!track.url || !isSafeUrl(track.url)) {
console.warn('Skipping track without valid HTTPS URL:', track.name);
continue;
}
// ...
audioUrl: track.url,
coverUrl: isSafeUrl(track.pic) ? track.pic : null, // Only store HTTPS URLs
Why Each Change Matters
-
track.urlguard (line 115): Adding!isSafeUrl(track.url)means any track whose audio URL is not HTTPS is skipped entirely. This is the right call: a track with afile://orhttp://audio URL is either malicious or broken, and neither case should be imported. -
coverUrlassignment (line 135): Cover art is optional metadata, so the fix doesn't skip the whole track iftrack.picis invalid — it simply storesnullinstead. This preserves the user experience (the track is still importable) while eliminating the SSRF vector for cover art URLs.
Together, these two changes ensure that no non-HTTPS URL ever enters the application's internal track representation, regardless of what the Meting API returns.
Prevention & Best Practices
1. Always Validate URLs from External Sources at the Point of Ingestion
The moment data arrives from a third-party API, treat it as untrusted. Apply validation before storing it, not at the point of use. In this codebase, the fix correctly validates URLs inside convertToLocalFormat(), which is the ingestion boundary.
2. Use the Native URL Constructor for Parsing
Avoid regex-based URL validation. The native URL constructor correctly handles edge cases like unusual schemes, encoded characters, and malformed inputs. It's also well-tested and maintained by the JavaScript runtime.
// ✅ Correct approach
function isSafeUrl(url) {
try {
return new URL(url).protocol === 'https:';
} catch {
return false;
}
}
// ❌ Fragile approach
const isHttps = url.startsWith('https://'); // Can be bypassed with HTTPS://
3. Consider Domain Allowlisting for Stronger Protection
The current fix enforces HTTPS, which blocks the most common SSRF vectors. For even stronger protection, consider allowlisting the specific CDN domains that Meting APIs are known to serve content from:
const ALLOWED_DOMAINS = ['music.163.com', 'y.qq.com', 'kugou.com'];
function isSafeUrl(url) {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:' && ALLOWED_DOMAINS.includes(parsed.hostname);
} catch {
return false;
}
}
This is a defense-in-depth measure that limits the blast radius if a trusted Meting server is ever compromised.
4. Apply Content Security Policy (CSP) Headers
For web applications, a strict Content-Security-Policy header can limit which domains the browser is permitted to fetch resources from, providing a second layer of defense even if a malicious URL slips through application-level validation.
5. Audit All URL Storage Points
Search your codebase for patterns where URLs from API responses are stored directly:
# Find potential URL storage patterns
grep -rn "\.url\s*=" src/
grep -rn "\.pic\s*=" src/
grep -rn "audioUrl\|coverUrl\|imageUrl" src/
Any assignment of an externally sourced URL to a stored field is a candidate for isSafeUrl() validation.
Relevant Standards
- OWASP SSRF Prevention Cheat Sheet: Recommends validating URL schemes, using allowlists, and blocking requests to internal IP ranges.
- CWE-918: Server-Side Request Forgery — the authoritative classification for this vulnerability class.
Key Takeaways
- The
convertToLocalFormat()function was the exact injection point: It processed raw Meting API responses and storedtrack.urlasaudioUrlandtrack.picascoverUrlwithout any protocol check — a single function was the entire attack surface. - Cover art URLs are just as dangerous as audio URLs: The
track.picfield had no validation at all before this fix. Attackers could use cover art URLs to probe internal HTTP services just as effectively as audio URLs. - The
URLconstructor is the right tool: Usingnew URL(url).protocolis more reliable than string prefix checks because it handles encoding, case normalization, and malformed inputs correctly. - Skipping vs. nulling is a deliberate UX choice: Tracks with invalid audio URLs are skipped entirely (no audio = no track), but tracks with invalid cover URLs are kept with
coverUrl: null(no art is acceptable, no audio is not). This distinction preserves user experience without sacrificing security. - Third-party API responses are untrusted input: Even APIs you intentionally integrate with can be compromised, intercepted, or replaced. Validate their output at the boundary, every time.
How Orbis AppSec Detected This
- Source: The
track.urlandtrack.picfields in the Meting API JSON response, received byfetchPlaylistWithFallback()and passed toconvertToLocalFormat() - Sink: The
audioUrl: track.urlandcoverUrl: track.picassignments inconvertToLocalFormat()atjs/cd-player/playlist-importer.js:135, where URLs are stored for later browser-initiated fetching - Missing control: No protocol validation, no domain allowlisting, and no scheme restriction on externally supplied URLs before storage
- CWE: CWE-918 — Server-Side Request Forgery (SSRF)
- Fix: Added
isSafeUrl()to enforcehttps:protocol using the nativeURLconstructor, applied to bothtrack.url(skip track if invalid) andtrack.pic(storenullif invalid)
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 vulnerability is a reminder that SSRF risks aren't limited to server-side code. In a client-side JavaScript application, any URL stored from an external source and later fetched by the browser carries the same risk profile: the browser becomes the HTTP client, and the user's local network becomes the attack surface.
The fix in playlist-importer.js is clean and minimal. A single isSafeUrl() function, applied at two precise points in convertToLocalFormat(), closes the vulnerability without changing any observable behavior for legitimate Meting API responses — which will always use HTTPS. The change is a good model for how to handle externally sourced URLs throughout any JavaScript application: validate at ingestion, use the native URL constructor, and enforce the strictest protocol you can justify.
If your application stores or fetches URLs from third-party APIs, now is a good time to audit those paths with the same lens.