CVEReports
Reports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Reports
  • Sitemap

Company

  • About
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Powered by Google Gemini & CVE Feed

|
•

GHSA-46h3-79wf-xr6c
CVSS 9.8|EPSS 0.04%

picklescan RCE: The "Secure Pickle" Myth and the _operator Bypass

Alon Barad
Alon Barad
Software Engineer•December 30, 2025•6 min read
PoC Available

Executive Summary (TL;DR)

picklescan, a tool designed to detect malicious Python pickles, failed to block the C-based `_operator` module. By using `_operator.attrgetter` instead of the blocked `operator.attrgetter`, attackers can bypass the scanner and achieve full RCE on systems attempting to verify untrusted serialized data.

A critical bypass in the picklescan security scanner allowing remote code execution via the Python C-implementation module '_operator'.

The Hook: Chasing the Dragon of "Safe" Pickles

In the world of Python security, the phrase "secure pickle" is usually the punchline to a bad joke. The pickle module is essentially a stack-based virtual machine that, by design, allows arbitrary code execution during deserialization. It is not a data format; it is a program.

Enter picklescan. This tool took on the Herculean task of trying to make pickles safe by statically analyzing the bytecode stream before loading it. It acts as a bouncer at the club, checking the ID of every function trying to get into memory. If it sees os.system or subprocess.Popen, it throws them out.

But here is the problem with blocklists (or "denylists"): you have to know every possible way to be malicious. If you miss one alias, one obscure built-in, or one C-extension, the bouncer steps aside, and the attacker walks right in. That is exactly what happened here. The developers blocked the front door, but they didn't realize the house had a back door labeled _operator.

The Flaw: The Evil Twin

To understand this bypass, you have to understand a quirk of Python's standard library. Many Python modules have a pure-Python implementation and a faster C-language implementation for performance. The operator module provides functional equivalents to standard operators (like +, -, or attribute access).

picklescan correctly identified that operator.attrgetter is dangerous. Why? Because attrgetter can be used to grab methods off objects. If you can import the os module, you can use attrgetter("system") to fetch the system function and run shell commands.

So, picklescan added operator to its list of unsafe_globals. Case closed, right? Wrong.

Python often exposes the C-implementation directly via an underscore prefix. While operator was blocked, _operator (the C-backed version) was not. They do the exact same thing. It is like banning "Robert" from your server but allowing "Bob" to have root access. The logic flaw wasn't in the scanning engine itself, but in the dictionary of known threats.

The Code: The Smoking Gun

The vulnerability lived entirely in src/picklescan/scanner.py. The scanner maintains a dictionary called _unsafe_globals. This is the "No Fly List" for pickle opcodes.

Here is what the code looked like before the fix. Notice it explicitly blocks operator but is blissfully unaware of its C-based sibling:

# OLD CODE (Vulnerable)
_unsafe_globals = {
    "operator": {
        "attrgetter",
        "itemgetter",
        "methodcaller",
    },
    # ... other modules ...
}

The fix, applied in commit f2dea43e0c838e09ace1e62994143254b51de927, was pitifully simple but absolutely necessary. They just had to tell the bouncer about "Bob":

# PATCHED CODE (Fixed)
_unsafe_globals = {
    "operator": {
        "attrgetter",
        "itemgetter",
        "methodcaller",
    },
    "_operator": {  # <--- The Fix
        "attrgetter",
        "itemgetter",
        "methodcaller",
    },
    # ...
}

[!NOTE] This highlights the inherent fragility of denylisting. The moment a new Python version introduces a new alias for a dangerous function, this scanner becomes obsolete instantly.

The Exploit: Crafting the Gadget

Let's construct the murder weapon. A pickle stream is just a sequence of opcodes. To exploit this, we need to construct a chain (gadget) that leads to os.system without ever explicitly typing os.system in a way the scanner recognizes.

Here is the attack chain:

  1. Import os: We use builtins.__import__ to load the os module. This puts the module object on the stack.
  2. Import _operator: We load the unblocked _operator module.
  3. Get attrgetter: We resolve _operator.attrgetter. The scanner allows this because _operator isn't in the bad list.
  4. Fetch system: We call attrgetter("system") and apply it to the os module object we loaded in step 1. This returns the actual os.system function.
  5. Execute: We call that function with our payload (e.g., id, whoami, or a reverse shell).

The raw pickle bytecode looks something like this:

# The bypass opcode sequence
opcode = b'''cbuiltins
__import__
(Vos
tRp0
0c_operator  <-- The Bypass
attrgetter
(Vsystem
tR(g0
tR(Vecho "You have been pwned"
tR.'''

When picklescan looks at this, it parses the tokens. It sees builtins.__import__ (which it might allow for innocuous modules) and _operator.attrgetter. Since _operator triggers no alarms, the payload passes validation. When pickle.loads() is finally called by the victim application believing the data is safe, the shell command executes immediately.

The Impact: Why This Matters

This vulnerability is particularly nasty because of where picklescan is used. It is primarily deployed in Machine Learning (ML) pipelines. ML models (like those from PyTorch) are often distributed as pickle files.

Hubs like Hugging Face or corporate model repositories rely on scanners to ensure that user-uploaded models aren't actually Trojan horses. A developer using picklescan < 0.0.34 believes they have sanitized the input. They will happily deserialize a model file provided by a third party, thinking their shield is up.

Successful exploitation means immediate Remote Code Execution (RCE) on the server processing the model. In an ML context, this usually means access to heavy GPU instances, proprietary training data, and potential lateral movement into the corporate cloud environment. It turns a data science workflow into a foothold for ransomware.

The Fix: Closing the Loophole

The immediate remediation is to upgrade picklescan to version 0.0.34. This version includes the updated blocklist covering _operator.

However, the deeper lesson here is about architecture. If you are relying on a Python script to scan a pickle file to determine if it is safe to execute, you have already lost. The complexity of the Python runtime means there will almost always be another gadget, another alias, or another memory corruption trick to bypass the scanner.

The Real Fix: Stop using pickles for untrusted data. Use JSON, Safetensors, or ONNX. If you absolutely must use pickles, do not rely on static scanning. execute them inside a disposable, network-isolated sandbox (like a microVM or a restricted container) where an RCE doesn't matter.

Official Patches

GitHub (picklescan)Commit fixing the vulnerability

Fix Analysis (1)

Technical Appendix

CVSS Score
9.8/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

picklescan < 0.0.34ML pipelines using picklescanModel repositories validating PyTorch files

Affected Versions Detail

ProductAffected VersionsFixed Version
picklescan
mmaitre314
< 0.0.340.0.34
AttributeDetail
CWECWE-502 (Deserialization of Untrusted Data)
CVSS9.8 (Critical)
Attack VectorNetwork / Local (via File)
ImpactRemote Code Execution (RCE)
Key Component_operator (Python C-Module)
Exploit StatusPoC Available

MITRE ATT&CK Mapping

MITRE ATT&CK Mapping

T1203Exploitation for Client Execution
Execution
T1059.006Command and Scripting Interpreter: Python
Execution
CWE-502
Deserialization of Untrusted Data

The application deserializes untrusted data without sufficiently verifying the resulting data will be valid.

Exploit Resources

Known Exploits & Detection

GitHubFunctional tests demonstrating the bypass using _operator.attrgetter

Vulnerability Timeline

Vulnerability Timeline

Vulnerability Discovered
2023-12-30
Fix Committed (v0.0.34)
2024-01-02
Advisory Published
2024-01-05

References & Sources

  • [1]GitHub Advisory GHSA-46h3-79wf-xr6c
  • [2]Python Operator Module Documentation
Related Intelligence
GHSA-955r-x9j8-7rhh

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.