VYPR
High severity8.1OSV Advisory· Published Oct 9, 2025· Updated Apr 15, 2026

CVE-2025-61773

CVE-2025-61773

Description

pyLoad is a free and open-source download manager written in Python. In versions prior to 0.5.0b3.dev91, pyLoad web interface contained insufficient input validation in both the Captcha script endpoint and the Click'N'Load (CNL) Blueprint. This flaw allowed untrusted user input to be processed unsafely, which could be exploited by an attacker to inject arbitrary content into the web UI or manipulate request handling. The vulnerability could lead to client-side code execution (XSS) or other unintended behaviors when a malicious payload is submitted. user-supplied parameters from HTTP requests were not adequately validated or sanitized before being passed into the application logic and response generation. This allowed crafted input to alter the expected execution flow. CNL (Click'N'Load) blueprint exposed unsafe handling of untrusted parameters in HTTP requests. The application did not consistently enforce input validation or encoding, making it possible for an attacker to craft malicious requests. Version 0.5.0b3.dev91 contains a patch for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
pyload-ngPyPI
< 0.5.0b3.dev910.5.0b3.dev91

Affected products

1

Patches

1
5823327d0b79

strengthen Input validation in captcha script and CNL Blueprint related to unsafe handling of untrusted input in the web UI (#4624)

https://github.com/pyload/pyloadJörmungandrkOct 2, 2025via ghsa
2 files changed · +36 28
  • src/pyload/webui/app/blueprints/cnl_blueprint.py+5 6 modified
    @@ -5,11 +5,11 @@
     from functools import wraps
     from urllib.parse import unquote
     
    +import flask
     from cryptography.hazmat.backends import default_backend
     from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    +from werkzeug.utils import secure_filename
     
    -import flask
    -from flask.json import jsonify
     from pyload.core.api import Destination
     from pyload.core.utils.convert import to_str
     from pyload.core.utils.misc import eval_js
    @@ -86,11 +86,10 @@ def addcrypted():
             "package", flask.request.form.get("source", flask.request.form.get("referer"))
         )
         dl_path = api.get_config_value("general", "storage_folder")
    -    dlc_filename = package.replace("/", "").replace("\\", "").replace(":", "") + ".dlc"
    -    dlc_path = os.path.join(dl_path, dlc_filename)
    -    dlc_path = os.path.normpath(dlc_path)
    +    dlc_filename = secure_filename(package) + ".dlc"
    +    dlc_path = os.path.abspath(os.path.join(dl_path, dlc_filename))
         # Ensure dlc_path is within dl_path
    -    if not os.path.abspath(dlc_path).startswith(os.path.abspath(dl_path) + os.sep):
    +    if not dlc_path.startswith(os.path.abspath(dl_path) + os.sep):
             return "failed: invalid package name\r\n", 400
         dlc = flask.request.form["crypted"].replace(" ", "+")
         with open(dlc_path, mode="wb") as fp:
    
  • src/pyload/webui/app/static/js/captcha-interactive.user.js+31 22 modified
    @@ -34,20 +34,29 @@
         along with this program.  If not, see <https://www.gnu.org/licenses/>.
     */
     
    -(function() {
    +(function () {
       'use strict';
     
       // this function listens to messages from the pyload main page
    -  window.addEventListener('message', function(e) {
    +  window.addEventListener('message', function (e) {
    +    // Restrict accepted origins to a trusted set.
    +    const trustedOrigins = [
    +      "https://pyload.net",    // Replace with your trusted parent origins
    +      // Add additional origins as needed.
    +    ];
    +    if (!trustedOrigins.includes(e.origin)) {
    +      // Ignore messages from untrusted or unknown origins
    +      return;
    +    }
         let request;
         try {
           request = JSON.parse(e.data);
    -    } catch(e) {
    +    } catch (e) {
           return
         }
    -    if(request.constructor === {}.constructor && request.actionCode === "pyloadActivateInteractive") {
    +    if (request.constructor === {}.constructor && request.actionCode === "pyloadActivateInteractive") {
           if (request.params.script) {
    -        const sig = new KJUR.crypto.Signature({"alg": "SHA384withRSA", "prov": 'cryptojs/jsrsa'});
    +        const sig = new KJUR.crypto.Signature({ "alg": "SHA384withRSA", "prov": 'cryptojs/jsrsa' });
             sig.init("-----BEGIN PUBLIC KEY-----\n" +
               "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuEHE4uAeTeEQjIwB//YH\n" +
               "Gl5e058aJRCRyOvApv1iC1ZQgXGHopgEd528+AtkAZKdCRkoNCWda7L/hROpZNjq\n" +
    @@ -60,16 +69,16 @@
             sig.updateString(request.params.script.code);
             if (sig.verify(request.params.script.signature)) {
               window.gpyload = {
    -            isVisible : function(element) {
    +            isVisible: function (element) {
                   const style = window.getComputedStyle(element);
                   return !(style.width === 0 ||
                     style.height === 0 ||
                     style.opacity === 0 ||
    -                style.display ==='none' ||
    +                style.display === 'none' ||
                     style.visibility === 'hidden'
                   );
                 },
    -            debounce : function (fn, delay) {
    +            debounce: function (fn, delay) {
                   let timer = null;
                   return function () {
                     const context = this, args = arguments;
    @@ -79,44 +88,44 @@
                     }, delay);
                   };
                 },
    -            submitResponse: function(response) {
    +            submitResponse: function (response) {
                   if (typeof gpyload.observer !== 'undefined') {
                     gpyload.observer.disconnect();
                   }
    -              const responseMessage = {actionCode: "pyloadSubmitResponse", params: {response: response}};
    -              parent.postMessage(JSON.stringify(responseMessage),"*");
    +              const responseMessage = { actionCode: "pyloadSubmitResponse", params: { response: response } };
    +              parent.postMessage(JSON.stringify(responseMessage), "*");
                 },
    -            activated: function() {
    -              const responseMessage = {actionCode: "pyloadActivatedInteractive"};
    -              parent.postMessage(JSON.stringify(responseMessage),"*");
    +            activated: function () {
    +              const responseMessage = { actionCode: "pyloadActivatedInteractive" };
    +              parent.postMessage(JSON.stringify(responseMessage), "*");
                 },
    -            setSize : function(rect) {
    +            setSize: function (rect) {
                   if (gpyload.data.rectDoc.left !== rect.left || gpyload.data.rectDoc.right !== rect.right || gpyload.data.rectDoc.top !== rect.top || gpyload.data.rectDoc.bottom !== rect.bottom) {
                     gpyload.data.rectDoc = rect;
    -                const responseMessage = {actionCode: "pyloadIframeSize", params: {rect: rect}};
    +                const responseMessage = { actionCode: "pyloadIframeSize", params: { rect: rect } };
                     parent.postMessage(JSON.stringify(responseMessage), "*");
                   }
                 },
    -            data : {
    +            data: {
                   debounceInterval: 1500,
    -              rectDoc: {top: 0, right: 0, bottom: 0, left: 0}
    +              rectDoc: { top: 0, right: 0, bottom: 0, left: 0 }
                 }
               };
     
               try {
                 let scriptFunction = new Function('request', 'gpyload', '"use strict";' + request.params.script.code);
                 scriptFunction(request, gpyload);
    -          } catch(err) {
    -            console.error("pyLoad: Script aborted: " + err.name + ": " + err.message + " (" + err.stack +")");
    +          } catch (err) {
    +            console.error("pyLoad: Script aborted: " + err.name + ": " + err.message + " (" + err.stack + ")");
                 return;
               }
               if (typeof gpyload.getFrameSize === "function") {
                 const checkDocSize = gpyload.debounce(() => {
    -              window.scrollTo(0,0);
    +              window.scrollTo(0, 0);
                   var rect = gpyload.getFrameSize();
                   gpyload.setSize(rect);
                 }, gpyload.data.debounceInterval);
    -            gpyload.observer = new MutationObserver(function(mutationsList) {
    +            gpyload.observer = new MutationObserver(function (mutationsList) {
                   checkDocSize();
                 });
                 const js_script = document.createElement("script");
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.