Insecure Deserialization via Unrestricted `torch.load()` Invocations in Model Loading and Merging Pipelines
[SECURITY ADVISORY] Insecure Deserialization in torch.load() Enables Arbitrary Code Execution via Checkpoints (CWE-502)
Title
Insecure Deserialization via Unrestricted torch.load() Invocations in Model Loading and Merging Pipelines
Summary
An Insecure Deserialization vulnerability (CWE-502) exists in Sygil-Dev/sygil-webui. Multiple components within the model management, inference, and model merging utilities deserialize PyTorch checkpoint files (.ckpt, .pt, .bin) using torch.load() without specifying weights_only=True. Because PyTorch model checkpoints rely on Python's pickle serialization format, loading an untrusted checkpoint allows arbitrary Python object reconstruction, leading to arbitrary code execution within the context of the running application.
Vulnerability Classification
- Vulnerability Type: Deserialization of Untrusted Data
- CWE ID: [CWE-502: Deserialization of Untrusted Data](https://cwe.mitre.org/data/definitions/502.html)
- Severity: Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H — Base Score: 8.8)
- Affected Package / Repository:
Sygil-Dev/sygil-webui
Affected Files
scripts/sd_utils/__init__.py(lines 328, 329, 577, 611, 1118, 2251, 2265, 3811)scripts/merge.py(lines 48, 49)scripts/webui.py(lines 471, 491)scripts/diffusers_textual_inversion_2.py(line 697)optimizedSD/optimized_txt2img.py(line 29)optimizedSD/optimized_img2img.py(line 29)ldm/models/diffusion/ddpm.py(line 254)ldm/models/autoencoder.py(lines 86, 371)
Technical Description & Root Cause
PyTorch's default serialization protocol uses Python's standard pickle engine. When torch.load() parses serialized data without restricting permitted classes, any custom reduction hook (__reduce__) defined within the pickle byte stream will be evaluated by the Python runtime during object instantiation.
In Sygil-Dev/sygil-webui, several user-facing entry points load model checkpoints directly from paths or downloads without weights_only=True:
1. Model Loading Routine (scripts/sd_utils/__init__.py:577)
def load_model_from_config(config, ckpt, verbose=False):
logger.info(f"Loading model from {ckpt}")
try:
# Vulnerable: Deserializes the file using unrestricted pickle unpickling
pl_sd = torch.load(ckpt, map_location="cpu")
sd = pl_sd["state_dict"] if "state_dict" in pl_sd else pl_sd2. Model Merging GUI (scripts/merge.py:48-49)
def merge(file1, file2, out, a):
# Vulnerable: Accepts arbitrary user file paths from GUI entry fields
model_0 = torch.load(file1)
model_1 = torch.load(file2)3. Textual Inversion Embeddings (scripts/sd_utils/__init__.py:2251)
def load_learned_embed_in_clip(learned_embeds_path, text_encoder, tokenizer, token=None):
# Vulnerable: Deserializes learned token embedding files without safe restrictions
loaded_learned_embeds = torch.load(learned_embeds_path, map_location="cpu")Because weights_only=True is not enabled, any checkpoint containing non-tensor object reductions is fully deserialized, triggering code execution during the file loading stage.
Impact
Arbitrary Code Execution: If a user loads, merges, or imports a third-party checkpoint file obtained from an untrusted source (such as community repositories, forums, or shared links), arbitrary code can execute with the privileges of the host process.
System Compromise: Full access to local files, environment variables, HuggingFace tokens, and local networks.
Remediation & Defensive Patch
Enable Safe Deserialization
Update all torch.load() calls across the repository to enforce weights_only=True:
--- a/scripts/sd_utils/__init__.py
+++ b/scripts/sd_utils/__init__.py
@@ -574,7 +574,7 @@ def load_model_from_config(config, ckpt, verbose=False):
logger.info(f"Loading model from {ckpt}")
try:
- pl_sd = torch.load(ckpt, map_location="cpu")
+ pl_sd = torch.load(ckpt, map_location="cpu", weights_only=True)
if "global_step" in pl_sd:
logger.info(f"Global Step: {pl_sd['global_step']}")
sd = pl_sd["state_dict"] if "state_dict" in pl_sd else pl_sdModel Merge
--- a/scripts/merge.py
+++ b/scripts/merge.py
@@ -325,8 +325,8 @@ def merge(file1, file2, out, weight):
out += ".ckpt"
try:
# Load Models
- model_0 = torch.load(file1)
- model_1 = torch.load(file2)
+ model_0 = torch.load(file1, weights_only=True)
+ model_1 = torch.load(file2, weights_only=True)Transition to .safetensors
Adopt the safetensors format for default storage and distribution of model weights, eliminating the reliance on Python pickle.
Source: Sygil-Dev/sygil-webui