Blind to the Obvious: Bypassing Fickling's Pickle Analyzer
Jan 10, 2026·5 min read
Executive Summary (TL;DR)
Fickling failed to track imports from Python's `builtins` module during decompilation. Attackers could craft pickle files that invoked `builtins.__import__` to load dangerous modules (like `os`) without triggering the security scanner's alarms. This effectively rendered the safety analysis useless against knowledgeable attackers.
Fickling, a security tool designed to reverse-engineer and analyze Python pickle files, contained a critical logic flaw that blinded it to built-in functions. By explicitly omitting `builtins` from its abstract syntax tree (AST) generation, the tool allowed attackers to execute arbitrary code using standard Python functions like `__import__` and `eval` while the analyzer reported the payload as 'LIKELY_SAFE'.
The False Sense of Security
Python's pickle module is notoriously insecure. It's less of a serialization format and more of a remote code execution engine disguised as a data structure. Enter Fickling, a tool by Trail of Bits designed to tame this beast. It decompiles pickle bytecode into a Python Abstract Syntax Tree (AST), allowing security scanners to statically analyze the contents before loading them. Ideally, this acts as a TSA checkpoint for your serialized data.
However, static analysis is a high-stakes game. If your analyzer misses a single path, the game is over. In CVE-2026-22612, we find a vulnerability that is almost poetic in its simplicity. The tool designed to catch malicious code was explicitly programmed to ignore the very building blocks of the language: the builtins.
This isn't a complex buffer overflow or a race condition. It is a fundamental logic error where the developers assumed that since built-in functions are always available, they didn't need to be tracked in the AST. Unfortunately, the safety scanner relied entirely on that tracking to identify dangerous behavior. It's like building a metal detector that specifically ignores steel.
The Invisible Hand
To understand the flaw, you have to look at how Fickling turns pickle opcodes into code. When the decompiler encounters an instruction to load a global object (like os.system or json.loads), it creates an AST node representing that import. The security scanner then traverses this tree, looking for a blacklist of UNSAFE_MODULES.
The vulnerability lies in fickling/fickle.py. The decompiler logic had a specific check: if the module being accessed was builtins, __builtin__, or __builtins__, it simply... did nothing. It skipped generating the ast.ImportFrom node entirely.
Why does this matter? Because the downstream analysis engine determines safety by checking which modules are imported. If you use builtins.__import__ to load the os module, the scanner expects to see an import node. Because Fickling suppressed that node, the scanner saw a function call but had no idea where it came from. To the analyzer, your malicious RCE payload looked just like a harmless function call to a local variable.
The Smoking Gun
Let's look at the code responsible for this blindness. This snippet from fickling/fickle.py shows the exact moment the decision was made to ignore the most dangerous part of the Python language.
Vulnerable Code:
# In fickling/fickle.py
module, attr = self.module, self.attr
# The fatal flaw: Explicitly ignoring builtins
if module in ("__builtin__", "__builtins__", "builtins"):
# "no need to emit an import for builtins!"
pass
else:
# Only non-builtins get an import node
alias = ast.alias(attr)
interpreter.module_body.append(
ast.ImportFrom(module=module, names=[alias], level=0)
)
interpreter.stack.append(ast.Name(attr, ast.Load()))The comment no need to emit an import for builtins! is the tombstone here. While syntactically true for running Python code (you don't need to import builtins to use them), it is catastrophic for static analysis, which needs to know the provenance of a symbol to determine if it is safe.
The Fix:
The patch is brutally simple: remove the special treatment. Treat builtins like any other module so the AST accurately reflects what is happening.
# The patched version simply treats everything equally
alias = ast.alias(attr)
interpreter.module_body.append(
ast.ImportFrom(module=module, names=[alias], level=0)
)
interpreter.stack.append(ast.Name(attr, ast.Load()))Crafting the Ghost Payload
Exploiting this requires constructing a pickle stream that uses builtins to bootstrap a full RCE chain. We can't just say import os, because Fickling would catch that. Instead, we use builtins.__import__.
Here is the attack chain:
- Use the
GLOBALopcode to fetchbuiltins.__import__. - Call it with the string
'os'. - Use
builtins.getattrto fetchsystemfrom the module we just loaded. - Execute
system('whoami').
Because of the bug, Fickling's AST generation generates function calls but omits the import statements that would identify these functions as dangerous. The scanner sees func_call('os') but doesn't know that func_call is actually __import__.
Here is a visualization of the bypass:
To make this robust, the reporter also noted that we need to satisfy liveness heuristics (dead code elimination might hide our payload). We can use the BUILD opcode to assign the result of our RCE (the exit code) to the state of a dummy object, ensuring the decompiler considers the code "live" and processes it.
The Fix
The remediation is straightforward: update Fickling. The maintainers pushed commit 9f309ab834797f280cb5143a2f6f987579fa7cdf which removes the logic that ignored builtins.
If you are using Fickling in a CI/CD pipeline to verify ML models or untrusted data, you are currently vulnerable to a trivial bypass. Update immediately.
However, the broader lesson here is about the fragility of denylist-based security. Trying to enumerate all "bad" things (unsafe modules) is difficult. Trying to do it on a dynamic language like Python, where eval, exec, and __import__ exist, is nearly impossible without perfect visibility. If you can avoid Pickle entirely, use formats like JSON or safetensors for ML models.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Fickling Trail of Bits | < Commit 9f309ab8 | Commit 9f309ab8 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-693 |
| Attack Vector | Network / Local (File-based) |
| CVSS | 8.1 (High) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | PoC Available |
| KEV Status | Not Listed |
MITRE ATT&CK Mapping
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.