CVE-2026-22606

Pickle Rick-rolled: Bypassing Fickling's Static Analysis with `runpy`

Alon Barad
Alon Barad
Software Engineer

Jan 10, 2026·6 min read

Executive Summary (TL;DR)

Fickling uses a blocklist to detect dangerous imports in Python pickles. It forgot to block `runpy`. Attackers can use `runpy.run_path()` to execute code, and Fickling marks it merely as "Suspicious" (due to unused variables) rather than "Overtly Malicious," allowing it to slip past security gates.

Fickling, the go-to tool for reverse-engineering and analyzing Python pickles for malware, failed to categorize the standard library module `runpy` as unsafe. This allowed attackers to craft malicious pickles that execute arbitrary code while bypassing high-severity security alerts.

The Hook: Taming the Pickle Beast

Python's pickle module is infamous in the security community. It's not just a serialization format; it's a stack-based virtual machine capable of executing arbitrary code during deserialization. For years, the advice has been simple: "Don't unpickle untrusted data." But in the age of ML models (PyTorch pth files, etc.), that advice is often ignored.

Enter Fickling. It's a brilliant tool designed to statically analyze pickle bytecode, decompile it into an Abstract Syntax Tree (AST), and flag malicious behavior without actually executing the payload. It's the TSA scanner for your serialized data bags. If it sees os.system('rm -rf /'), it screams. It's a critical line of defense for companies ingesting user-uploaded AI models.

But here's the catch with static analysis: it has to know exactly what evil looks like. If the definition of "evil" is incomplete, the scanner waves the bomb right through. CVE-2026-22606 is exactly that scenario—a blind spot in the scanner's threat definitions that turns a safety tool into a false sense of security.

The Flaw: The Problem with Badness Enumeration

The vulnerability lies in how Fickling determines if a pickle is dangerous. It iterates through the decompiled AST and checks imports against a hardcoded list of "unsafe" modules. This is the classic "allow-list vs. block-list" problem. Fickling chose a block-list.

The developers correctly identified the usual suspects: os, subprocess, pty, marshal. These are the modules you use if you want to pop a shell. However, Python's standard library is vast and full of dark corners. They missed runpy.

runpy is a standard library module used to locate and execute Python modules without importing them first. Crucially, it exposes run_path() and run_module(). These functions can execute arbitrary code just as effectively as os.system, but because runpy wasn't on the naughty list, Fickling's analyzer didn't trigger the OVERTLY_MALICIOUS flag. Instead, it defaulted to a heuristic check. Since the return value of the malicious function usually isn't used in a payload (it's just executed for side effects), Fickling flagged it as SUSPICIOUS (Unused Variable). In many automated pipelines, "Suspicious" is logged and ignored; "Overtly Malicious" is blocked. That difference in severity is the entire exploit.

The Code: Adding `runpy` to the Naughty List

The fix is embarrassingly simple, highlighting the fragility of block-lists. We can look directly at fickling/fickle.py to see the logic gap.

Before the Patch (The Vulnerable Logic):

def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]:
    for node in self.nodes:
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            # checking against the blocklist
            if node.module in (
                "os",
                "subprocess",
                "pty",
                "marshal",
                "types",
                # ... crickets ...
            ):
                yield node

If the AST node module was runpy, the generator yielded nothing. The safety check passed.

The Fix (Commit 9a2b3f89):

            if node.module in (
                "os",
                "subprocess",
                "pty",
                "marshal",
                "types",
                "runpy",  # <--- The million dollar line
            ):
                yield node

By adding that single string, Fickling now recognizes that importing runpy is inherently dangerous in a serialized context.

The Exploit: Constructing the Payload

To exploit this, we don't need fancy memory corruption. We just need to speak the language of the Pickle VM. We need to tell the VM to import runpy.run_path and call it with a path to a script we control (or a malicious file dropped on disk via other means).

Here is how we craft the bypass manually using Python's pickletools opcodes. This simulates the VM stack operations:

import pickle
# The payload opcodes
opcode_chain = [
    b'\x80\x05',                # PROTO 5
    b'\x8c\x05runpy',           # Pushing string "runpy"
    b'\x8c\x08run_path',        # Pushing string "run_path"
    b'\x93',                    # STACK_GLOBAL (Import runpy.run_path)
    b'\x8c\x12/tmp/malicious.py', # Push the argument
    b'\x85',                    # Build a tuple (args)
    b'\x52',                    # REDUCE (Call the function)
    b'.',                       # STOP
]
payload = b''.join(opcode_chain)

The Attack Flow:

  1. Preparation: Attacker ensures a malicious script exists at /tmp/malicious.py (or uses a relative path if they have control over the working directory).
  2. Delivery: Attacker uploads this pickle to a service protected by Fickling (e.g., a "Safe Model Scanner").
  3. Analysis: Fickling decompiles it. It sees import runpy. It checks the blocklist. runpy is not there. It marks the import as safe.
  4. Heuristics: Fickling sees the result of run_path is discarded. It flags Severity.SUSPICIOUS.
  5. Execution: The pipeline decides "Suspicious is probably fine" and unpickles the data. runpy executes the script. Game over.

The Impact: When Security Tools Fail

The impact here is subtle but critical. This isn't just an RCE; it's an RCE specifically targeting the mechanism designed to prevent RCE. Organizations deploy Fickling because they know they are handling dangerous data. They trust the tool's judgment.

When that trust is violated via a false negative, the downstream systems execute the payload with zero hesitation. The primary targets are AI/ML infrastructure providers, model zoos (like Hugging Face), and security pipelines that audit Python artifacts. A successful exploit grants the attacker code execution context of the unpickling process, often leading to container escape, data exfiltration of proprietary models, or poisoning of training pipelines.

The Fix: Patch and Pivot

The immediate remediation is to upgrade Fickling to version 0.1.7. This version includes the updated blocklist.

However, this vulnerability should serve as a wake-up call regarding the nature of Python pickling. A blocklist approach will always play catch-up. There are likely other modules or obscure ways to trigger code execution that haven't been blocked yet.

Real Mitigation Strategies:

  1. Switch Formats: If possible, move to safetensors or ONNX for model weights. These formats are designed to be safe and purely data-driven, unlike pickle.
  2. Sandboxing: Never unpickle data—even "scanned" data—on a production host with access to sensitive keys. Use ephemeral, network-isolated sandboxes (like gVisor or Firecracker microVMs) to perform the deserialization.
  3. Audit Logs: If you must stick with pickle and Fickling, treat Severity.SUSPICIOUS with the same paranoia as Severity.OVERTLY_MALICIOUS until verified.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.9/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Affected Systems

Fickling <= 0.1.6Automated malware scanning pipelines using FicklingAI/ML Model Security ScannersSupply chain security tools analyzing PyPI packages

Affected Versions Detail

Product
Affected Versions
Fixed Version
Fickling
Trail of Bits
<= 0.1.60.1.7
AttributeDetail
CWE IDCWE-184 (Incomplete List of Disallowed Inputs)
Attack VectorNetwork (AV:N)
CVSS v4.08.9 (Critical)
Vulnerability TypeStatic Analysis Bypass
Exploit StatusPoC Available
ImpactArbitrary Code Execution (RCE)
CWE-184
Incomplete List of Disallowed Inputs

The product does not properly validate input, or uses a blocklist that misses critical dangerous elements.

Vulnerability Timeline

Fix committed to GitHub repository
2026-01-07
CVE-2026-22606 Published
2026-01-10
Fickling v0.1.7 Released
2026-01-10

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.