StanzaのレマタイザーにおけるpickleデシリアライズRCE(CVE-2026-115878)
Stanza 1.14.0はレマタイザーのチェックポイント内のgzip圧縮pickleを制限なく読み込むため、細工したモデルでコードが実行されます。1.15.0で修正済みです。
要点
- 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のフォールバックとは別の問題だと述べています。
どのように悪用されますか?
- 攻撃者は
dicts_versionが2で、dicts値がgzip圧縮されたpickleである.ptチェックポイントを作成します。pickleの__reduce__は呼び出し可能オブジェクトとその引数、たとえばos.systemとコマンドを返します。 - 被害者のアプリケーションは、そのファイルを指す
lemma_model_pathでパイプラインを構築するか、攻撃者が書き込める共有レジストリやモデルディレクトリから読み込みます。 torch.load(..., weights_only=True)は外側のチェックポイントを受け入れます。dicts値はbytesオブジェクトなので、外側の許可リストは内部を調べません。_unpack_pos_dictがバイト列を展開し、pickle.loadsを呼びます。pickleストリームはデシリアライズ中に呼び出し可能オブジェクトを実行します。- パイプラインがテキストを処理する前に、モデルを読み込むユーザーとしてコマンドが実行されます。
アドバイザリには最小のPoCがあります。ペイロードクラスは仕組みを示すのに十分短いものです。
class Payload:
def __reduce__(self):
return (os.system, ("id > /tmp/stanza_lemma_poc_pwned",))
アドバイザリは、ペイロードがStanza 1.14.0、PyTorch 2.13.0、CPython 3.13.7に対して実行され、マーカーファイルにコマンド出力が記録されたと述べています。PoCは_unpack_pos_dictを直接呼び出し、ソースレビューによってstanza.Pipeline経由の経路も確立しています。
攻撃の流れは?
呼び出し元の影響を受けるチェックポイントが、外側のweights_only許可リストを通過した入れ子のpickleを制限のないpickle.loadsに運び、モデルを読み込むユーザーとして攻撃者のコマンドを実行します。
攻撃者に何ができますか?
チェックポイントを提供できる攻撃者は、それを読み込むプロセスの権限で任意のコードを実行します。ノートブックではノートブックのユーザー、サービスではサービスアカウント、CIジョブではリポジトリとそのシークレットにアクセスできるビルドユーザーです。
コードはパイプラインの構築中、アプリケーションが文書を1つも処理する前に実行されます。モデルファイル、ジョブが到達できるデータセット、環境変数、クラウド資格情報、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ログとノートブックのログで、Stanzaのモデルリポジトリ以外のホストからのモデルダウンロードを確認します。既定のパイプラインはプロジェクトのURLからダウンロードします。
- モデルの読み込み時に現れたプロセス、ファイル、外向き接続を探します。PoCはシェルコマンドを実行するため、子プロセスが監査ログに現れる場合があります。
悪意あるチェックポイントが読み込まれた場合は、ホストが侵害されたものとして扱います。読み込みプロセスが読めたすべてのシークレット、クラウドキー、APIトークン、Hugging Faceトークンをローテーションします。単一のファイルを削除するのではなく、既知の正常なイメージから環境を再構築します。
年表
修正は1.14.0の14日後に到着しましたが、リリースまで64日待ちました。
- 2026-07-15:Stanza 1.14.0がgzip-pickle辞書形式バージョン2でリリース。
- 2026-07-29:修正コミットがpickleをorjsonに置き換え、バージョン3形式を追加。
- 2026-10-01:Stanza 1.15.0が修正とともにリリース。
- 2026-10-23:CVE識別子を予約。
- 2026-11-03:GitHubアドバイザリとCVEレコードを公開。
よくある質問
すべてのStanzaバージョンが影響を受けますか?
いいえ。脆弱なバージョン2形式を書き込み、読み込むのは1.14.0だけです。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日確認)