id: PYSEC-2026-492 summary: pyLoad allows upload to arbitrary folder lead to RCE details: "### Summary\nAn authenticated user can change the download folder and upload\ \ a crafted template to the specified folder lead to remote code execution\n\n###\ \ Details\nexample version: 0.5\nfile:src/pyload/webui/app/blueprints/app_blueprint.py\n\ ```python\n@bp.route(\"/render/\", endpoint=\"render\")\ndef render(filename):\n\ \ mimetype = mimetypes.guess_type(filename)[0] or \"text/html\"\n data = render_template(filename)\n\ \ return flask.Response(data, mimetype=mimetype)\n```\nSo, if we can control\ \ file in the path \"pyload/webui/app/templates\" in latest version and path in\ \ \"module/web/media/js\"(the difference is the older version0.4.20 only renders\ \ file with extension name \".js\"), the render_template func will works like SSTI(server-side\ \ template injection) when render the evil file we control.\n\nin /settings page\ \ and the choose option general/general, where we can change the download folder.\ \ \n![image](https://github.com/pyload/pyload/assets/48705773/0b239138-9aaa-45c4-bf84-c1c3103c452a)\n\ \ \nAlso, we can find the pyLoad install folder in /info page\n![image](https://github.com/pyload/pyload/assets/48705773/6e9d363a-f0e0-4d25-92b3-b1587188a235)\n\ \ So, we can change the value of Download folder to the template path. Then through\ \ /json/add_package we can upload a crafted template file to RCE.\n```python\n@bp.route(\"\ /json/add_package\", methods=[\"POST\"], endpoint=\"add_package\")\n# @apiver_check\n\ @login_required(\"ADD\")\ndef add_package():\n api = flask.current_app.config[\"\ PYLOAD_API\"]\n\n package_name = flask.request.form.get(\"add_name\", \"New Package\"\ ).strip()\n queue = int(flask.request.form[\"add_dest\"])\n links = [l.strip()\ \ for l in flask.request.form[\"add_links\"].splitlines()]\n pw = flask.request.form.get(\"\ add_password\", \"\").strip(\"\\n\\r\")\n\n try:\n file = flask.request.files[\"\ add_file\"]\n\n if file.filename:\n if not package_name or package_name\ \ == \"New Package\":\n package_name = file.filename\n\n \ \ file_path = os.path.join(\n api.get_config_value(\"general\"\ , \"storage_folder\"), \"tmp_\" + file.filename\n )\n file.save(file_path)\n\ \ links.insert(0, file_path)\n\n except Exception:\n pass\n\ \n urls = [url for url in links if url.strip()]\n pack = api.add_package(package_name,\ \ urls, queue)\n if pw:\n data = {\"password\": pw}\n api.set_package_data(pack,\ \ data)\n\n return jsonify(True)\n```\n### PoC\nFirst login into the admin page,\ \ then visit the info page to get the path of pyload installation folder.\nSecond,\ \ change the download folder to PYLOAD_INSTALL_DIR/ webui/app/templates/\nThird,\ \ upload crafted template file through /json/add_package through parameter add_file\n\ the content of crafted template file and its filename is \"341.html\":\n```\n{{x.__init__.__globals__['__builtins__']['eval'](\"\ __import__('os').popen('whoami').read()\")}}\n```\n![image](https://github.com/pyload/pyload/assets/48705773/a933a95b-bb18-4e2e-a442-973585e7d1fc)\n\ \ Last, visit http://TARGET/render/tmp_341.html to trigger the RCE\n![image](https://github.com/pyload/pyload/assets/48705773/80a1ba00-2774-4ce5-bc9e-dd32f189634e)\n\ \ ![image](https://github.com/pyload/pyload/assets/48705773/136236f2-9b00-4506-a8ac-29a14a537bbe)\n\ \ \n### Impact\nIt is a RCE vulnerability and I think it affects all versions. In\ \ earlier version 0.4.20, the trigger difference is the pyload installation folder\ \ path difference and the upload file must with extension \".js\" .\nThe render\ \ js code in version 0.4.20:\n```python\n@route(\"/media/js/\"\ )\ndef js_dynamic(path):\n response.headers['Expires'] = time.strftime(\"%a,\ \ %d %b %Y %H:%M:%S GMT\",\n time.gmtime(time.time()\ \ + 60 * 60 * 24 * 2))\n response.headers['Cache-control'] = \"public\"\n \ \ response.headers['Content-Type'] = \"text/javascript; charset=UTF-8\"\n\n try:\n\ \ # static files are not rendered\n if \"static\" not in path and\ \ \"mootools\" not in path:\n t = env.get_template(\"js/%s\" % path)\n\ \ return t.render()\n else:\n return static_file(path,\ \ root=join(PROJECT_DIR, \"media\", \"js\"))\n except:\n return HTTPError(404,\ \ \"Not Found\")\n```" aliases: - CVE-2024-32880 - GHSA-3f7w-p8vr-4v5f modified: '2026-07-09T17:40:16.770673Z' published: '2026-06-29T11:50:39.394067Z' references: - type: WEB url: https://github.com/pyload/pyload/security/advisories/GHSA-3f7w-p8vr-4v5f - type: ADVISORY url: https://nvd.nist.gov/vuln/detail/CVE-2024-32880 - type: PACKAGE url: https://github.com/pyload/pyload - type: PACKAGE url: https://pypi.org/project/pyload-ng - type: ADVISORY url: https://github.com/advisories/GHSA-3f7w-p8vr-4v5f affected: - package: name: pyload-ng ecosystem: PyPI purl: pkg:pypi/pyload-ng ranges: - type: ECOSYSTEM events: - introduced: '0' - last_affected: 0.5.0 versions: - 0.5.0a5.dev528 - 0.5.0a5.dev532 - 0.5.0a5.dev535 - 0.5.0a5.dev536 - 0.5.0a5.dev537 - 0.5.0a5.dev539 - 0.5.0a5.dev540 - 0.5.0a5.dev545 - 0.5.0a5.dev562 - 0.5.0a5.dev564 - 0.5.0a5.dev565 - 0.5.0a6.dev570 - 0.5.0a6.dev578 - 0.5.0a6.dev587 - 0.5.0a7.dev596 - 0.5.0a8.dev602 - 0.5.0a9.dev615 - 0.5.0a9.dev629 - 0.5.0a9.dev632 - 0.5.0a9.dev641 - 0.5.0a9.dev643 - 0.5.0a9.dev655 - 0.5.0a9.dev806 - 0.5.0b1.dev1 - 0.5.0b1.dev2 - 0.5.0b1.dev3 - 0.5.0b1.dev4 - 0.5.0b1.dev5 - 0.5.0b2.dev10 - 0.5.0b2.dev11 - 0.5.0b2.dev12 - 0.5.0b2.dev9 - 0.5.0b3.dev100 - 0.5.0b3.dev13 - 0.5.0b3.dev14 - 0.5.0b3.dev17 - 0.5.0b3.dev18 - 0.5.0b3.dev19 - 0.5.0b3.dev20 - 0.5.0b3.dev21 - 0.5.0b3.dev22 - 0.5.0b3.dev24 - 0.5.0b3.dev26 - 0.5.0b3.dev27 - 0.5.0b3.dev28 - 0.5.0b3.dev29 - 0.5.0b3.dev30 - 0.5.0b3.dev31 - 0.5.0b3.dev32 - 0.5.0b3.dev33 - 0.5.0b3.dev34 - 0.5.0b3.dev35 - 0.5.0b3.dev38 - 0.5.0b3.dev39 - 0.5.0b3.dev40 - 0.5.0b3.dev41 - 0.5.0b3.dev42 - 0.5.0b3.dev43 - 0.5.0b3.dev44 - 0.5.0b3.dev45 - 0.5.0b3.dev46 - 0.5.0b3.dev47 - 0.5.0b3.dev48 - 0.5.0b3.dev49 - 0.5.0b3.dev50 - 0.5.0b3.dev51 - 0.5.0b3.dev52 - 0.5.0b3.dev53 - 0.5.0b3.dev54 - 0.5.0b3.dev57 - 0.5.0b3.dev60 - 0.5.0b3.dev62 - 0.5.0b3.dev64 - 0.5.0b3.dev65 - 0.5.0b3.dev66 - 0.5.0b3.dev67 - 0.5.0b3.dev68 - 0.5.0b3.dev69 - 0.5.0b3.dev70 - 0.5.0b3.dev71 - 0.5.0b3.dev72 - 0.5.0b3.dev73 - 0.5.0b3.dev74 - 0.5.0b3.dev75 - 0.5.0b3.dev76 - 0.5.0b3.dev77 - 0.5.0b3.dev78 - 0.5.0b3.dev79 - 0.5.0b3.dev80 - 0.5.0b3.dev81 - 0.5.0b3.dev82 - 0.5.0b3.dev85 - 0.5.0b3.dev87 - 0.5.0b3.dev88 - 0.5.0b3.dev89 - 0.5.0b3.dev90 - 0.5.0b3.dev91 - 0.5.0b3.dev92 - 0.5.0b3.dev93 - 0.5.0b3.dev94 - 0.5.0b3.dev95 - 0.5.0b3.dev96 - 0.5.0b3.dev97 - 0.5.0b3.dev98 - 0.5.0b3.dev99 - 0.5.0b3.dev101 severity: - type: CVSS_V3 score: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H