markdown

はじめに (対象読者・この記事でわかること)

この記事は、ファイルアップロード機能を持つWebサービスを開発・運用しているエンジニアを対象にしています。
この記事を読むことで、zip爆弾を実際に展開せずに「これは危険かもしれない」と検出するPythonスクリプトの作り方がわかります。サードパーティライブラリに頼らず、標準ライブラリだけで実装するため、セキュリティ面でも安心です。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Pythonの基本的な文法 - ZIPファイルの構造の初歩的な知識(「圧割比」という言葉を聞いたことがあれば十分)

zip爆弾とは何か、なぜ検出が必要なのか

zip爆弾とは、見た目は数十KBのzipファイルなのに、展開するとGB・TB単位の巨大なファイルが出現してしまう悪意のあるファイルです。
Webサービスでユーザーがアップロードしたzipを自動展開する機能がある場合、サーバーのディスクやメモリを圧迫され、DoS(サービス拒否)攻撃を受ける恐れがあります。
「展開してみないと中身がわからない」ようでは手遅れです。そこで、展開前に「これは怪しい」と判断するロジックが必要になります。

zip爆弾を展開せずに検出するPython実装

標準ライブラリzipfileだけで、圧縮前のファイルサイズ(file_size)と圧縮後のファイルサイズ(compress_size)を比較することで、圧割比を計算できます。
以下のスクリプトは、圧割比が閾値(例:100倍)を超えるエントリが1つでもあれば「zip爆弾の疑いあり」と判定します。

ステップ1:zipfileモジュールでメタデータを読む

Python
import zipfile import sys THRESHOLD = 100 # 圧割比100倍を超えたら危険とみなす def is_zip_bomb(path: str, threshold: int = THRESHOLD) -> bool: try: with zipfile.ZipFile(path) as zf: for info in zf.infolist(): # ディレクトリエントリはスキップ if info.is_dir(): continue # 圧縮前が0バイトのファイルは除く(0除算防止) if info.file_size == 0: continue ratio = info.file_size / info.compress_size if ratio > threshold: print( f"detect suspicious entry: {info.filename} " f"ratio={ratio:.1f} " f"uncompressed={info.file_size} " f"compressed={info.compress_size}" ) return True except zipfile.BadZipFile: # zipではないファイルも「検出対象外」として扱う return False return False if __name__ == "__main__": target = sys.argv[1] if len(sys.argv) > 1 else "test.zip" if is_zip_bomb(target): print("Result: zip bomb suspected") sys.exit(1) else: print("Result: clean") sys.exit(0)

ステップ2:閾値を調整する

100倍でも十分に厳しいですが、サービスの用途によってはもっと緩くてもよい場合があります。
例えば、ログファイルのように同じ文字が繰り返されるデータは、圧縮率が極端に高くなることもあるため、閾値を1000倍に緩めるケースもあります。
閾値を引数で外から与えられるようにしておくと、運用時に設定ファイルや環境変数でチューニングしやすくなります。

ステップ3:大きなヘッダを許容しない

zip爆弾の中には、「ローカルファイルヘッダのファイル名長」や「エクストラフィールド長」にでたらめな巨大な値を入れて、パーサを混乱させるものもあります。
zipfileモジュールは内部でこれらの値をチェックしてくれますが、一応自前で上限を設けておくとより安全です。

Python
MAX_FILENAME_LEN = 4096 MAX_EXTRA_FIELD_LEN = 65536 def is_sane_entry(info: zipfile.ZipInfo) -> bool: return ( len(info.filename) <= MAX_FILENAME_LEN and info.extra <= MAX_EXTRA_FIELD_LEN )

ハマった点と解決策

問題:「圧縮後サイズが0になっている」エントリがあった

zip爆弾の作者は、「compress_size = 0」にしておくと、無限大の圧割比になってバグを誘発しようとする場合があります。
上記コードではinfo.file_size == 0の場合にcontinueしていますが、compress_size == 0の場合はinfo.file_size > 0であれば無条件に「危険」と判定するようにするとよいでしょう。

解決策

Python
if info.compress_size == 0: # 圧縮後0バイトは無理がある return True

まとめ

本記事では、zip爆弾を展開せずに検出するPythonスクリプトの実装例を紹介しました。

  • zipfile標準ライブラリだけで、圧縮前/圧縮後のサイズ比を計算できる
  • 閾値(例:100倍)を超えるエントリがあれば「zip爆弾の疑い」と判定する
  • 運用時に閾値を調整しやすくするため、引数または環境変数で与える設計にする

この記事を通して、ファイルアップロード機能を持つWebサービスで、zip爆弾によるDoS攻撃リスクを大幅に削減できるようになりました。
次回は、これを非同期処理(FastAPIやCelery)に組み込む方法や、ZIP以外のフォーマット(7z、tar.gz)にも対応する方法を紹介する予定です。

参考資料