GitHub Security Advisory: GHSA-mxhj-88fx-4pcv
Package: fickling (PIP)
Severity: HIGH (CVSS 0.0)
CWE: CWE-436: Interpretation Conflict
Vulnerable versions: < 0.1.8
Patched in: 0.1.8

Summary: Fickling: OBJ opcode call invisibility bypasses all safety checks

Details:
# Assessment

The interpreter so it behaves closer to CPython when dealing with `OBJ`, `NEWOBJ`, and `NEWOBJ_EX` opcodes (https://github.com/trailofbits/fickling/commit/ff423dade2bb1f72b2b48586c022fac40cbd9a4a).

# 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 use the OBJ opcode to call dangerous stdlib functions (signal handlers, network servers, network connections, file operations). The OBJ opcode's implementation in fickling pushes function calls directly onto the interpreter stack without persisting them to the AST via `new_variable()`. When the result is discarded with POP, the call vanishes from the final AST entirely, making it invisible to all 9 analysis passes.

This is a separate vulnerability from the REDUCE+BUILD bypass, with a different root cause. It survives all three proposed fixes for the REDUCE+BUILD vulnerability.

## Details

The vulnerability is a single missing `new_variable()` call in `Obj.run()` (`fickle.py:1333-1350`).

**REDUCE** (`fickle.py:1286-1301`) correctly persists calls to the AST:
```python
# Line 1300: call IS saved to module_body
var_name = interpreter.new_variable(call)
interpreter.stack.append(ast.Name(var_name, ast.Load()))
```

The comment on lines 1296-1299 explicitly states: "if we just save it to the stack, then it might not make it to the final AST unless the stack value is actually used."

**OBJ** (`fickle.py:1333-1350`) does exactly what that comment warns against:
```python
# Line 1348: call is ONLY on the stack, NOT in module_body
interpreter.stack.append(ast.Call(kls, args, []))
```

When the OBJ result is discarded by POP, the `ast.Call` is gone. The decompiled AST shows the import but no function call:
```python
from smtplib import SMTP    # import present (from STACK_GLOBAL)
result = None              # no call to SMTP visible
```

Yet at runtime, `SMTP('127.0.0.1')` executes and opens a TCP connection.

**NEWOBJ** (`fickle.py:1411-1420`) and **NEWOBJ_EX** (`fickle.py:1423-1433`) have the same code pattern but are less exploitable since CPython's NEWOBJ calls `cls.__new__()` (allocation only) while OBJ calls `cls(*args)` (full constructor execution with `__init__` side effects).

### Affected versions

All versions through 0.1.7 (latest as of 2026-02-19).

### 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 pickle that opens a TCP connection to an attacker's ser