RustのzipクレートCVE-2026-115905、9.0.5でも整合性チェックは無効
目次
TL;DR
影響 中央ディレクトリから読む ZipArchive で検査し、ローカルファイルヘッダーから読む ZipStreamReader で展開する構成で、検査一覧にないファイルが展開先に書き込まれる問題。展開先のスクリプトが実行される環境ではリモートコード実行(RCE)の恐れ。
修正版 zip 9.0.5以降(影響は9.0.4以前)。ただし9.2.2まで、ヘッダー整合性チェックは既定で無効のまま。更新しただけでは防御できない。
対応 9.1.0以降へ更新し、信頼できないZIPを扱う箇所で enforce_header_consistency(true) を明示。RustSec未登録のため cargo audit では検出不可(2026年11月2日時点)。
Rustで広く使われているZIPライブラリ zip クレート(Rustのパッケージ、zip-rs/zip2)に、脆弱性CVE-2026-115905が公開された。
GitHubのアドバイザリ GHSA-m665-v7pm-7grjが付けたCVSS v4.0(脆弱性の深刻度スコア)の基本値は最高値の10.0で、影響範囲は9.0.4以前、修正版は9.0.5から提供されている。
NVDの登録は2026年10月27日(日本時間)で、11月2日時点ではNVD側の評価は「Awaiting Analysis(分析待ち)」のため、10.0はGitHub側の暫定スコアとなる。
修正版では整合性チェックが追加されたものの、既定で無効になっているほか、バージョンごとの挙動の違いもある。
ZIPにはファイル名が2か所に記録されている
ZIPファイルの中では、1つのファイルの情報が2か所に記録されている。
| 場所 | 位置 | 書かれている内容 |
|---|---|---|
| ローカルファイルヘッダー(LFH) | 各ファイルの圧縮データの直前 | ファイル名、圧縮方式、サイズなど |
| 中央ディレクトリ(CDFH) | ZIPファイルの末尾にまとまった目次 | 同じくファイル名、サイズ、各LFHの位置など |
本来は両方に同じファイル名が入るが、ZIPの仕様上、両者の一致確認は読み手に義務付けられていないため、ツールによってどちらを参照するかが異なる。
末尾の目次を先に読めるツールは中央ディレクトリから取得し、先頭から順にストリーム展開するツールは、目次を読み出す前にローカルファイルヘッダーを使って展開する。
2つのAPIでファイル名が食い違う
zipクレートでは、APIによって参照するヘッダーが分かれていた。
| API | 参照するヘッダー | 主な用途 |
|---|---|---|
ZipArchive | 中央ディレクトリ | ファイル全体を読んでの一覧取得・検査・展開 |
ZipStreamReader | ローカルファイルヘッダー | 先頭から順に流し読みしながらの展開 |
ZipArchive は中央ディレクトリから名前を取得し、ZipStreamReader は先頭から読み進めてローカルファイルヘッダーの名前でファイルを作成する。両者の照合は行われていなかった。
そのため、中央ディレクトリには doc_report.pdf、ローカルファイルヘッダーには cron_worker.sh と書いたZIPを作ると、APIごとに別のファイル名として認識される。
アドバイザリの実証コード(PoC)は、zipクレート自身で cron_worker.sh を含めたZIPを作成し、末尾の中央ディレクトリにあるファイル名の14バイトだけを同じ長さの doc_report.pdf に書き換えている。圧縮データやCRC(誤り検出符号)は変更していない。
flowchart TD
A["攻撃者が細工ZIPをアップロード<br/>目次はdoc_report.pdf<br/>LFHはcron_worker.sh"] --> B["検査側がZipArchiveで一覧を取得"]
B --> C["doc_report.pdfしか見えず<br/>PDFのみ許可のルールを通過"]
C --> D["展開側がZipStreamReaderで展開"]
D --> E["展開先のcron_worker.shが<br/>攻撃者の内容で上書き"]
E --> F["そのスクリプトが実行される環境ならRCE"]
PoCでは、拡張子 .pdf のみを許可する検査関数を ZipArchive で作成し、検査を通過したZIPを ZipStreamReader::extract で展開している。
展開先にあらかじめ置いた cron_worker.sh が、攻撃者の内容で置き換わる挙動を確認できる。
任意ファイル上書きの影響範囲
アドバイザリにある「任意ファイル上書き」は、展開先ディレクトリの中で指定した名前のファイルを上書きできる、という意味になる。
ZipStreamReader::extract は書き込み前にパスを正規化し、展開先の外へ出る名前(../ を含むものなど)を弾くため、パストラバーサルの制限自体は外れない。
PoCも展開先にある cron_worker.sh を上書きする例になっている。
展開先に実行されるスクリプトや設定ファイルが同居している場合、被害が大きくなりやすい。
アドバイザリでは、ユーザーから送られたZIPを自動展開するバックエンドや、ウイルス検査・拡張子許可リストによる検査を展開とは別の工程で行う構成を影響例として挙げている。
もう1つの前提は、展開側で ZipStreamReader を使っていることだ。
このAPIは zip::unstable::stream という非安定モジュールに置かれている。ZipArchive だけで検査も展開も行うコードなら、このAPI間の食い違いによる影響は受けない。
ただし、検査をRust以外のツール(中央ディレクトリを読む製品など)で行い、展開だけをzipクレートの ZipStreamReader で行う構成でも同様のすり抜けが起こる。
修正版での整合性チェックの初期設定
修正版では、ローカルファイルヘッダーと中央ディレクトリの内容が一致するかを確かめる enforce_header_consistency 設定が追加され(修正コミット)、ファイル名や圧縮方式、更新日時、CRC、サイズなどを照合している。
しかし、この設定は既定で無効になっている。
9.0.5の ZipStreamReader::new のドキュメントコメントには「後方互換のため、ローカルヘッダーが中央ディレクトリと一致するかは確かめない」と記載され、実装も enforce_header_consistency: false で初期化される。
2026年11月2日時点の最新版9.2.2でも同様で、ZipArchive 側の ZipReadOptions や ExtractOptions も既定は無効のままだ。
そのため、cargo update で9.0.5以降へ更新しただけでは既存コードの動作は変わらない。
整合性チェックを有効にするには、展開側で明示的に指定する。
use zip::unstable::stream::ZipStreamReader;
ZipStreamReader::new(reader)
.enforce_header_consistency(true)
.extract(&dest_dir)?;
検査側では、ZipArchive::validate_header_consistency() を呼べば、ヘッダー間の食い違いをエラーとして取得できる(9.0.5から利用可能)。
検査で中央ディレクトリの一覧を読む前にこれを実行し、不一致のZIPを弾いておけば、展開側の実装にかかわらず細工ZIPの通過を防げる。
let mut archive = zip::ZipArchive::new(reader)?;
archive.validate_header_consistency()?; // 食い違いがあればErr
for i in 0..archive.len() {
// ここから一覧の検査
}
9.0.5の挙動と9.1.0での変更
ZipStreamReader は先頭から順にストリーム処理するため、照合に必要な中央ディレクトリはZIPの最後まで読み終えないと取得できない。
9.0.5の実装を確認すると、各ファイルを展開先へ書き込みながらヘッダー情報を保持し、最後に中央ディレクトリと突き合わせてエラーを返す構成になっている。
そのため、整合性チェックを有効にしてエラーが返った時点でも、ファイルはすでに書き込まれている。extract のドキュメントにも「展開はアトミックではなく、エラー時に一部のファイルがディスクに残ることがある」と記載されている。
9.0.5のコメントにはロールバックの記述があるが、修正コミットの履歴で部分展開のロールバック処理は削除されており、9.0.5のコードにも書き込み済みファイルを消す処理は含まれていない。
9.1.0(2026年10月24日リリース)からは、整合性チェックを有効にすると、展開先内に作成した一時ディレクトリへ展開し、照合が通った場合だけ本来の場所へ移動する実装へ変更された。
展開側で整合性チェックに頼る場合は、9.1.0以降へ更新して有効にする形が安全だ。
| バージョン | 整合性チェックの既定 | 有効にしたときの ZipStreamReader::extract |
|---|---|---|
| 9.0.4以前 | 設定なし | 照合しない |
| 9.0.5 | 無効 | 展開先へ書いたあとに照合し、不一致ならエラー(書き込み済みファイルは残る) |
| 9.1.0〜9.2.2 | 無効 | 一時ディレクトリへ展開して照合し、一致したものだけ移動 |
自前処理を行う ZipStreamVisitor を使っている場合は、visitorが照合前に各ファイルを受け取るため、visit が Ok を返すまではファイル内容に基づく処理を実行しないよう注意がいる。
修正版の要件とcargo auditの検出状況
9.0.5の Cargo.toml では、必要なRustのバージョン(rust-version)が1.88に指定されており、9.2.2でも変わらない。古いツールチェーンを使っている場合は、あらかじめRust本体の更新がいる。
依存関係の脆弱性を調べる cargo audit は、RustSecのアドバイザリデータベースを参照する。
2026年11月2日時点で、RustSecのzipクレートの登録は過去の3件(RUSTSEC-2026-0367〜0369)のみで、GHSA-m665-v7pm-7grjに対応するエントリは未登録だ。
そのため、現時点で cargo audit を実行しても今回の脆弱性は警告されない。GitHub Advisory Databaseを参照するDependabotなどでは通知が届く。
なお登録済みの3件(SUID/SGIDビット保持、展開先外へのシンボリックリンク、リンクを通じた権限変更)は9.0.0で修正済みのため、9.0.0未満を使っている場合はこれらの影響も受ける。
依存関係とコードの確認
まず、プロジェクト内のどの依存関係がzipクレートを使っているかを確認する。
cargo tree -i zip
Cargo.lock に複数のzipバージョンが含まれている場合は、cargo tree -i zip@<バージョン> のように指定して確認できる。
間接的な依存として入っている場合、依存先が ZipStreamReader を整合性チェックなしで呼び出していれば影響を受けるため、依存ライブラリ側の更新状況もあわせて見ておく。
自前のコード内で ZipStreamReader を使っている箇所や、ZIPの検査と展開を別のAPIで行っている箇所は、grepなどで検索して確認する。
grep -rn "ZipStreamReader\|unstable::stream" --include='*.rs' .
該当箇所があれば、9.1.0以降へ更新したうえで enforce_header_consistency(true) を指定し、検査側では validate_header_consistency() で不一致ZIPを弾く設定を入れておく。展開先ディレクトリを実行スクリプトや設定ファイルの置き場と分離しておく構成も有効だ。