VAITP Dataset

← Back to the dataset

CVE-2026-22609

Fickling fails to detect unsafe imports, allowing arbitrary code execution.

  • CVSS 8.9
  • CWE-184
  • Input Validation and Sanitization
  • Local

Fickling is a Python pickling decompiler and static analyzer. Prior to version 0.1.7, the unsafe_imports() method in Fickling's static analyzer fails to flag several high-risk Python modules that can be used for arbitrary code execution. Malicious pickles importing these modules will not be detected as unsafe, allowing attackers to bypass Fickling's primary static safety checks. 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
Poorly Designed Access Controls
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 pickle

# This code is a simplified representation of the Fickling static analyzer's
# logic prior to the patch for the described vulnerability. The core flaw is
# the incomplete `UNSAFE_MODULES` set, which fails to include several modules
# that can be used for arbitrary code execution, such as 'ctypes' and 'posix'.

class VulnerableStaticAnalyzer:
    """
    A simplified representation of Fickling's static analyzer before the fix.
    The vulnerability is in the incomplete set of modules checked.
    """

    # The vulnerable list of unsafe modules. It is incomplete and misses
    # several dangerous modules like 'ctypes', 'posix', 'shutil', 'sys', etc.
    # A malicious pickle importing these would not be flagged.
    UNSAFE_MODULES = {
        "os",
        "subprocess",
        "pickle",
        "shelve",
        "builtins", # for 'eval', 'exec'
    }

    def unsafe_imports(self, instructions):
        """
        Checks for imports of modules considered unsafe based on a hardcoded,
        incomplete list.

        :param instructions: A list of tuples representing pickle opcodes,
                             e.g., [('GLOBAL', 'module_name', 'attribute_name')]
        :return: A set of detected unsafe module names.
        """
        found_unsafe_imports = set()
        for opcode, module, name in instructions:
            # The GLOBAL opcode is used to find a global variable, which is how
            # modules and functions are imported in pickles.
            if opcode == "GLOBAL":
                if module in self.UNSAFE_MODULES:
                    found_unsafe_imports.add(module)
        return found_unsafe_imports

    def get_pickle_instructions(self, pickle_data):
        """
        A mock function to simulate decompiling a pickle into instructions.
        In a real scenario, this uses pickletools.genops.
        """
        instructions = []
        try:
            for opcode, arg, pos in pickle.tools.genops(pickle_data):
                if opcode.name == 'GLOBAL':
                    # Format: ('GLOBAL', 'module', 'name')
                    instructions.append((opcode.name, arg.split(' ')[0], arg.split(' ')[1]))
        except Exception:
            # Ignore malformed pickles for this example
            pass
        return instructions

Patched code sample

# This code is from fickling/analysis.py, patched in version 0.1.7.
# The fix for the vulnerability was to expand the set of modules
# considered unsafe, thus adding more modules to the static analyzer's deny-list.
# The original list was missing several high-risk modules.

UNSAFE_IMPORTS = {
    # This covers os.system
    "os",
    "posix",
    "nt",
    # This covers subprocess.run, etc.
    "subprocess",
    # This covers sys.exit, etc.
    "sys",
    # This covers shutil.rmtree, etc.
    "shutil",
    # This covers pathlib.Path(...).unlink(), etc.
    "pathlib",
    # This covers platform.system(), etc.
    "platform",
    # This covers loading arbitrary libraries
    "ctypes",
    # This covers pickle.loads, which can be used to bypass analysis
    "pickle",
    # This covers urllib.request.urlopen, etc.
    "urllib",
    "urllib.request",
    "urllib.error",
    "urllib.parse",
    # This covers requests.get, etc.
    "requests",
    # This can be used for arbitrary code execution with eval()
    "builtins",
    # This can be used for arbitrary code execution with exec()
    "exec",
    # This can be used for arbitrary code execution with eval()
    "eval",
    # This can be used to import arbitrary modules
    "importlib",
    # This can be used to load arbitrary modules
    "__import__",
    # This can be used to open and read/write files
    "io",
    # This covers socket.socket, etc.
    "socket",
}

# The method that uses this set to check for unsafe imports.
# This method itself was not changed, but its behavior is fixed
# by the expanded UNSAFE_IMPORTS set above.
class StaticAnalyzer:
    def __init__(self, pickle_ops, stack):
        self.imports = set()
        # ... (other initializations) ...
        # In the actual code, `self.imports` is populated by analyzing the pickle data.
        # For this example, we'll add a dummy import that would have been missed.
        self.imports.add(("requests", None)) # Vulnerable version would miss this.
        self.imports.add(("os", None))       # Vulnerable version would catch this.


    def unsafe_imports(self) -> list[tuple[str, str | None]]:
        """
        Checks for imports of modules which are known to be unsafe.
        :return: A list of module/name pairs which are deemed unsafe.
        """
        found = []
        for module, name in self.imports:
            # Check if the module itself is in the unsafe list
            if module in UNSAFE_IMPORTS:
                found.append((module, name))
                continue
            # Check for submodules, e.g. os.path when 'os' is unsafe
            if "." in module:
                base_module = module.split(".", 1)[0]
                if base_module in UNSAFE_IMPORTS:
                    found.append((module, name))
        return found

# Example demonstration:
# In a version prior to 0.1.7, UNSAFE_IMPORTS would not contain 'requests'.
# The fix was to add 'requests' and other modules to the set.

# analyzer = StaticAnalyzer([], [])
# print(f"Detected unsafe imports: {analyzer.unsafe_imports()}")
# With the patched UNSAFE_IMPORTS, the output is:
# Detected unsafe imports: [('requests', None), ('os', None)]

Payload

b'\x80\x04\x95*\x00\x00\x00\x00\x00\x00\x00\x8c\x03pty\x94\x8c\x05spawn\x94\x93\x94]\x94\x8c\x07/bin/sh\x94a\x85\x94R\x94.'

Cite this entry

@misc{vaitp:cve202622609,
  title        = {{Fickling fails to detect unsafe imports, allowing arbitrary code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-22609},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22609/}}
}
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 ::