flawopen.com/パストラバーサル/Zip Slip
Zip Slip 脆弱性とは?
ZIP の内部には ../../outside.txt のような名前を含められます。展開処理がその名前を信頼すると、プロセスの権限が及ぶ範囲で展開先の外へファイルを書き込めます。
見落としやすい理由
展開処理はありふれていますが、信頼できない ZIP や TAR の各エントリ名は作成者が制御します。パスを検証せず outputDir + entry.name と連結すると、アーカイブ内容を通じたディレクトリ脱出が起きます。
for entry in zip_file:
dest = os.path.join(output_dir, entry.name)
write_file(dest, entry.read())
# entry.name は "../../outside.txt" の可能性がある
root = os.path.realpath(output_dir)
os.makedirs(root, exist_ok=True)
for entry in zip_file:
dest = os.path.realpath(
os.path.join(root, entry.name)
)
try:
inside_root = os.path.commonpath([root, dest]) == root
except ValueError:
inside_root = False
if not inside_root:
raise ValueError("Unsafe entry path")
write_file(dest, entry.read())
この検査は、文字列の接頭辞だけが共通する隣接パスも含め、ルート外への逸脱を防ぎます。新規の非公開ディレクトリに展開し、symlink エントリを拒否してください。既存 symlink や並行したファイルシステム変更への対策には、競合に強い API または保守された展開ライブラリが必要です。
よくある質問
Python のアーカイブ API は異なるパス保護を提供する
上のコード比較は独自の展開ループを示す概念例であり、Python 組み込みの ZipFile.extractall() ではありません。Python のドキュメントによると、ZipFile.extract() と extractall() は絶対パスや .. による展開先からの逸脱を防ごうとしますが、信頼できないアーカイブは事前に検査するよう警告しています。zipfile.Path はメンバー名をサニタイズしないため、呼び出し側で検証が必要です。TAR の展開フィルターは Python 3.12 で追加され、Python 3.14 では既定値が filter="data" になりました。既定値がより緩い可能性のある対応ランタイムでは、このフィルターを明示してください。data フィルターでも、あらゆる危険な動作やリソース枯渇を防げるわけではありません。新しい非公開の一時ディレクトリに展開し、アーカイブサイズ、メンバー数、展開後の合計サイズ、処理時間に上限を設けてください。
2018 年にこの手法が広く知られた後、保護を追加したバージョンもありますが、既定でパスを検証しないものもあります。使用中のバージョンの文書を確認し、各エントリを明示的に検証してください。