Stanza 词形还原器 pickle 反序列化远程代码执行(CVE-2026-115878)
Stanza 1.14.0 会无限制地加载词形还原器检查点中的 gzip pickle,因此特制模型会执行任意代码;已在 1.15.0 中修复。
TL;DR
- Stanza 1.14.0 将词形还原器词典以 gzip 压缩 pickle 的形式保存在模型检查点中,然后用不受限制的
pickle.loads加载。特制检查点会在加载模型时执行代码。 - 只有 Stanza 1.14.0 受影响。当应用程序加载受调用方影响的
lemma_model_path或来自共享注册表的模型时,该路径可达。 - GitHub 公告以 CVSS v3.1 本地向量和用户交互评为 7.8 High。公告包含经过端到端确认的概念验证(PoC)。目前没有已知利用,截至 2026 年 11 月 4 日该 CVE 不在 CISA KEV 中。
- 升级到 Stanza 1.15.0 或更高版本。修复将内部 pickle 替换为 orjson,并拒绝易受攻击的检查点格式;模型转换步骤请参见“如何修复?”。
我受影响吗?
必须同时满足以下所有条件:
- 安装的 Stanza 版本为 1.14.0。这是唯一写入和读取 gzip-pickle
pos_dict格式的版本。 - 代码从攻击者能够影响的路径加载词形还原器检查点。例如
lemma_model_path参数、共享模型目录、自定义模型注册表或接受上传模型的制品存储。 - 攻击者能够放置或替换代码加载的
.pt文件。通过 Stanza 默认下载注册表加载模型不是暴露路径,因为该流程会校验 MD5 校验和。
公告的 PoC 直接调用易受攻击的 _unpack_pos_dict 函数,并通过源码审查确认了经文档化 stanza.Pipeline(..., lemma_model_path=...) API 的路径。加载发生在构建管道期间。
| 版本 | 受影响 | 修复版本 |
|---|---|---|
| 1.14.0 | 是,当加载受调用方影响的检查点时 | 1.15.0 |
| 1.13.0 及更早 | 否,这些版本使用旧版元组格式 | 不适用 |
| 1.15.0 及更高 | 否,这些版本使用 JSON v3 格式 | 继续更新 |
检查已安装的版本:
python -c "import stanza; print(stanza.__version__)"
pip show stanza
pip freeze | grep -i '^stanza=='
对于容器,请查询正在运行的容器,而不是新拉取的镜像:
docker exec <container> python -c "import stanza; print(stanza.__version__)"
然后找出所有提供模型路径的位置:
grep -rn "lemma_model_path" .
grep -rn "lemma" --include="*.py" . | grep -i "model_path\|checkpoint"
在用户或任务之间共享的自定义模型目录是风险最高的位置。请把对该目录的写权限视为在每个从中加载模型的主机上执行代码。
如何修复?
- 将 Stanza 升级到 1.15.0 或更高版本:
pip install --upgrade "stanza>=1.15.0"
- 使用项目所用工具更新锁文件:
pip-compile --upgrade-package stanza requirements.in
poetry update stanza
uv lock --upgrade-package stanza
- 重建每个固定 Stanza 的容器和虚拟环境。升级会改变磁盘上的词典格式,因此仍包含 1.14.0 的旧镜像仍然易受攻击。
- 重新下载 Stanza 默认模型。1.15.0 拒绝加载版本 2 检查点,并抛出指明转换器的
ValueError。对于自定义模型,运行随附的转换器:
python -m stanza.utils.lemma.convert_lemma_dict old_model.pt --output new_model.pt
- 确认安装版本为 1.15.0 或更高,并且管道加载的是转换后的模型。
如果无法升级,请在安排升级期间降低暴露面:
- 只加载已对照自己控制的值校验 SHA-256 的模型文件。
- 限制对模型目录及为其提供文件的任何注册表的写权限。
- 优先使用会校验 MD5 校验和的 Stanza 默认下载流程,而不是调用方提供的路径。
- 使用低权限账户运行模型加载任务,并且不授予云凭据。
这些措施只能减少能够提供文件的人。它们不能让 pickle.loads 调用变得安全,公告的 PoC 对 1.14.0 仍然有效。
根本原因是什么?
Stanza 是 Stanford NLP 小组开发的 Python 自然语言处理库。在 1.14.0 中,词形还原器将词性词典 pos_dict 保存为从词性到“单词到词元”映射的嵌套映射。
训练器用 pickle.dumps 序列化该映射,用 gzip 压缩结果,并将字节保存在检查点的 dicts 字段中。易受攻击的代码位于 v1.14.0 标签的 stanza/models/lemma/trainer.py 第 54 至 59 行:
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()
return pickle.loads(raw)
Trainer.load 在第 367 行用 torch.load(..., weights_only=True) 读取外部检查点。该标志将外部 pickle 限制为原始类型和张量。bytes 值是原始类型,因此内部 gzip 流会通过外部允许列表而不被检查。第 378 行随后将这些字节交给 _unpack_pos_dict:
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:
self.pos_dict = _unpack_pos_dict(checkpoint['dicts'])
pickle 流按全局名称存储一个可调用对象及其调用参数,pickle.loads 会查找该可调用对象并调用它。恶意流可以指定任何可调用对象,包括 os.system。公告的 PoC 用一个返回 (os.system, (command,)) 的 __reduce__ 方法构造该流,命令在 _unpack_pos_dict 执行时运行。
外部的 weights_only=True 在这里不是防御。危险操作是内部的 pickle.loads,它接收外部加载器视为不透明字节串的数据。公告指出,这与 CVE-2026-54499 中修复的 weights_only=False 回退是不同的问题。
如何被利用?
- 攻击者构造一个
.pt检查点,其dicts_version为 2,dicts值是 gzip 压缩的 pickle。pickle 的__reduce__返回一个可调用对象及其参数,例如os.system和一条命令。 - 受害应用程序用指向该文件的
lemma_model_path构建管道,或从攻击者可写的共享注册表或模型目录加载它。 torch.load(..., weights_only=True)接受外部检查点。dicts值是bytes对象,因此外部允许列表不会检查其内部。_unpack_pos_dict解压字节并调用pickle.loads。pickle 流在反序列化期间调用该可调用对象。- 命令在以加载模型的用户身份运行,早于管道处理任何文本。
公告包含一个最小 PoC。其 payload 类足够短,可以展示机制:
class Payload:
def __reduce__(self):
return (os.system, ("id > /tmp/stanza_lemma_poc_pwned",))
公告指出,payload 已针对 Stanza 1.14.0、PyTorch 2.13.0 和 CPython 3.13.7 运行,标记文件记录了命令输出。PoC 直接调用 _unpack_pos_dict,并通过源码审查确认了经 stanza.Pipeline 的路径。
攻击如何展开?
受调用方影响的检查点将嵌套 pickle 送过外部 weights_only 允许列表,进入不受限制的 pickle.loads,从而以加载模型的用户身份执行攻击者的命令。
攻击者能做什么?
能够提供检查点的攻击者以加载它的进程权限执行任意代码。在 notebook 中,这是 notebook 用户;在服务中,这是服务账户;在 CI 任务中,这是可访问仓库及其密钥的构建用户。
代码在构建管道期间运行,早于应用程序处理任何文档。它可以读取模型文件、任务可访问的数据集、环境变量、云凭据和 API 令牌。它还可以写入文件系统或启动一个在加载结束后继续运行的进程。
在构建模型制品的 CI 任务中,加载可能在测试之前运行,读取环境中的 CI 令牌并推送到仓库。模型托管服务会向任何能上传检查点的人暴露同一原语,即使其推理 API 要求认证。
条件与“我受影响吗?”相同:Stanza 1.14.0、来自攻击者影响路径的检查点,以及放置或替换该文件的能力。CVSS 向量为本地且需要用户交互,CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H,因为受害者必须加载该文件。攻击者不需要 Stanza 安装的凭据。
目前没有已知的在野利用。公告包含经过端到端确认的 PoC,发现者已针对发布包、PyTorch 2.13.0 和 CPython 3.13.7 进行测试。
为什么修复有效?
1.15.0 的修复从词典路径中移除了 pickle。发布提交 b21e1c9b47b4fccff21d3b0a6c0687a19410c2f0 将序列化器改为 orjson,将反序列化器改为 orjson.loads:
def _pack_pos_dict(pos_dict):
- raw = pickle.dumps(pos_dict, protocol=4)
+ raw = orjson.dumps(pos_dict)
...
def _unpack_pos_dict(data):
- return pickle.loads(raw)
+ return orjson.loads(raw)
同一提交引入新格式版本并拒绝旧格式:
-_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
...
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."
+ )
orjson.loads 将 JSON 数据解析为字典、列表、字符串和数字。它不能指定可调用对象或重建任意对象,因此特制词典无法执行代码。拒绝版本 2 可防止攻击者对已修复的安装使用旧的 pickle 格式。
修复对已演示的路径是完整的。它同时改变写入和读取,并拒绝旧检查点,而不是尝试净化它们。代价是版本 2 的自定义模型必须重新下载或使用 convert_lemma_dict.py 转换。
如何检查是否已遭入侵?
目前没有针对该漏洞的公开活动指标。公告的 PoC 仅写入标记文件以证明执行,因此标记路径不是生产指标。
请改为检查模型供应链:
- 列出 Stanza 管道加载的每个
.pt文件,并将其来源与模型注册表或下载日志比对。没有记录来源的文件是线索。 - 检查
lemma_model_path值和任何共享模型目录的近期变更。新增或替换的检查点就是投递机制。 - 检查 CI 和 notebook 日志中是否有从 Stanza 模型仓库以外主机下载模型的记录。默认管道从项目自己的 URL 下载。
- 查找加载模型时出现的进程、文件或出站连接。PoC 会运行 shell 命令,因此子进程可能出现在审计日志中。
如果加载了恶意检查点,请将主机视为已遭入侵。轮换加载进程能够读取的所有密钥,包括云密钥、API 令牌和 Hugging Face 令牌。从已知良好的镜像重建环境,而不是只删除一个文件。
时间线
修复在 1.14.0 之后 14 天完成,但又等待了 64 天才发布。
- 2026-07-15:Stanza 1.14.0 发布,采用版本 2 的 gzip-pickle 词典格式。
- 2026-07-29:修复提交用 orjson 替换 pickle,并加入版本 3 格式。
- 2026-10-01:Stanza 1.15.0 随修复发布。
- 2026-10-23:预留 CVE 标识符。
- 2026-11-03:GitHub 公告和 CVE 记录发布。
常见问题
所有 Stanza 版本都受影响吗?
不是。只有 1.14.0 写入和读取易受攻击的版本 2 格式。1.13.0 及更早使用旧版元组格式,1.15.0 及更高使用 JSON 版本 3 格式。
weights_only=True 能保护我吗?
不能。外部 torch.load 调用受到限制,但 dicts 字段是 bytes 对象,外部允许列表不会检查它。危险调用是 _unpack_pos_dict 内部的 pickle.loads。
这和 CVE-2026-54499 是同一个问题吗?
不是。CVE-2026-54499 涉及早期加载路径中的 weights_only=False 回退。该漏洞仍位于 weights_only=True 路径内,并滥用嵌套字节流。
我需要重新下载模型吗?
版本 2 检查点需要。Stanza 1.15.0 会拒绝它们,并抛出指向 convert_lemma_dict.py 的 ValueError。请重新下载默认模型,或在加载前转换自定义模型。
该漏洞在野被利用了吗?
截至 2026 年 11 月 4 日,没有公开的已知利用。公告包含经过端到端确认的 PoC,该 CVE 不在 CISA KEV 中。
共享模型目录有多大风险?
它对每个可写入该目录的账户都是一次代码执行。能够替换 .pt 文件的用户,可以冒充后续加载该文件的每个用户或服务身份执行代码,因此请将该目录视为特权代码路径。
用 Ostorlab 发现易受攻击的 Stanza 安装
Ostorlab 的软件组成分析和 SBOM会清点应用程序中的 Python 包,并标记清单和锁文件实际解析到的版本。用它找出 Stanza 1.14.0 以及需要升级到 1.15.0 的模型加载路径。
参考文献
- GitHub,“Stanza: RCE via pickle deserialization in the lemmatizer”(GHSA-gh9r-c94j-cmp5,2026 年 11 月 3 日发布)。https://github.com/advisories/GHSA-gh9r-c94j-cmp5 (2026 年 11 月 4 日核实)
- CVE Program,“CVE-2026-115878”(2026 年 11 月 3 日发布)。https://www.cve.org/CVERecord?id=CVE-2026-115878 (2026 年 11 月 4 日核实)
- Stanford NLP,“Patch a security hole by using orjson to encode/decode the lemma dictionary instead of pickle”(提交 b21e1c9b,2026 年 7 月 29 日提交)。https://github.com/stanfordnlp/stanza/commit/b21e1c9b47b4fccff21d3b0a6c0687a19410c2f0 (2026 年 11 月 4 日核实)
- Stanford NLP,“Use orjson for the lemma dictionary”(拉取请求 1649)。https://github.com/stanfordnlp/stanza/pull/1649 (2026 年 11 月 4 日核实)
- Stanford NLP,“Stanza 1.15.0”(2026 年 10 月 1 日发布)。https://github.com/stanfordnlp/stanza/releases/tag/v1.15.0 (2026 年 11 月 4 日核实)
- Stanford NLP,“stanza/models/lemma/trainer.py at v1.14.0”(易受攻击标签处的源码)。https://github.com/stanfordnlp/stanza/blob/v1.14.0/stanza/models/lemma/trainer.py (2026 年 11 月 4 日核实)
- PyPI Stats,“stanza”(每周下载量,2026 年 11 月 4 日核实)。https://pypistats.org/packages/stanza (2026 年 11 月 4 日核实)
- CISA,“Known Exploited Vulnerabilities Catalog”(2026 年 11 月 4 日核实订阅源)。https://www.cisa.gov/known-exploited-vulnerabilities-catalog (2026 年 11 月 4 日核实)