圧縮爆弾(解凍爆弾)の仕組みと対策、展開後のサイズに上限をかけるアップロードとAPIの守り方
対象の目安: ファイルや圧縮データを扱うWebアプリやAPIの開発者 / 実務


圧縮爆弾(解凍爆弾、英語ではdecompression bombやzip bomb)は、小さな圧縮データを相手に展開させ、展開後の巨大なデータでメモリやディスクを使い果たさせる攻撃です。アップロードの上限を数MBにしていても、展開した後の大きさを制限していなければ、サーバーは数GBのデータを抱え込むことになります。
Webアプリで展開が起きる場所は、ZIPのアップロードに限りません。HTTPの圧縮された要求本文、外部のサーバーから受け取る圧縮された応答、PNGなどの画像の読み込みも、内部では展開の処理です。この記事は、ファイルや圧縮データを扱うWebアプリやAPIの開発者に向けて、圧縮爆弾が成立する仕組み、展開が起きる入り口、展開後のサイズと件数に上限をかける実装を、MITREのCWE、OWASP、各言語とフレームワークの公式ドキュメントに沿って整理します。
圧縮爆弾は展開後の大きさで資源を使い果たさせる攻撃
圧縮は、繰り返しの多いデータを短い記述に置き換える仕組みです。同じバイトが延々と続くデータは、ごく短い圧縮データで表せます。圧縮爆弾はこの性質を逆手に取り、展開後の大きさを確かめずに展開する処理へ、極端に圧縮率の高いデータを渡します。
MITREはこの弱点を、CWE-409「圧縮率の高いデータの不適切な処理(データ増幅)」として分類しています。圧縮率が極端に高く、展開すると大きな出力を生む入力を、製品が適切に扱わないことを指します。資源の消費を適切に制御しない弱点は、CWE-400「制御されない資源の消費」に分類されます。どちらの弱点も、メモリ不足による異常終了、ディスクの枯渇、処理の長時間化につながります。
1回の展開で膨らむ量と、構造で膨らませる手口
zlibの技術資料によると、ZIPやgzipが使うdeflate形式の理論上の最大圧縮率は1032対1です。1MBの圧縮データが、1回の展開で約1GBに膨らみうる計算になります。
1回の展開だけでなく、データの構造で膨らみを掛け算にする手口もあります。
- ZIPの中にZIPを入れ、さらにその中にZIPを入れる入れ子の構造にする。すべての階層を自動で展開する処理では、階層の数だけ膨らみが掛け算になります
- 1つのアーカイブに大量のファイルを入れる。合計の大きさが小さくても、ファイルの作成やメタデータの処理で時間とディスクを使います
- 展開後のサイズを小さく偽ったヘッダーを付ける。ヘッダーの値を見て許可する実装は、実際に展開してから想定外の大きさに気付きます
XMLの実体参照を入れ子にして展開させるXML爆弾も、同じ「小さな入力が展開時に巨大化する」型の攻撃です。XMLの処理で起きる問題は、次の記事で整理しています。
あわせて読みたい
XXEとは何か。XML外部実体参照で起きる情報漏えいの仕組みとDTD無効化による根本対策
Webアプリで展開が起きる入り口
圧縮爆弾の対策は、自分のアプリケーションのどこで展開が起きているかを把握することから始まります。開発者が明示的に展開のコードを書いていない場所でも、ライブラリやフレームワークが自動で展開している場合があります。
| 入り口 | 展開が起きる場面 | 見落としやすい点 |
|---|---|---|
| ファイルのアップロード | ZIPの取り込み、Officeファイル(docx、xlsx)の解析 | docxやxlsxは中身がZIP形式で、解析ライブラリが内部で展開します |
| 圧縮された要求本文 | Content-Encoding: gzipやbrの要求を、フレームワークが自動で展開する | 上限が展開前と展開後のどちらに働くかはフレームワークによって違います |
| 外部から受け取る応答 | Webhookの受け取り先やURLのプレビュー機能で、HTTPクライアントが応答を自動で展開する | 通信先を利用者が指定できると、攻撃者のサーバーが細工した応答を返せます |
| 画像の読み込み | PNGなどの画像をデコードして縮小やサムネイルを作る | ファイルが小さくても、縦横の画素数が巨大だと展開後のメモリが膨らみます |
OWASPのFile Upload Cheat Sheetも、アップロード機能への攻撃の例にZIP爆弾とXML爆弾を挙げています。そのうえで、ファイルを展開したり処理したりする場合、サイズの上限は展開後の大きさで考え、ZIPのサイズは安全な方法で計算するよう求めています。アップロード機能全体の守り方は、次の記事で整理しています。
あわせて読みたい
ファイルアップロード機能の脆弱性と対策。Webシェルを置かせないための検証と保存の設計
対策の基本は展開後の量で上限をかけること
圧縮爆弾への対策は、展開する側が「どこまでなら受け入れるか」を決め、それを展開の途中で強制することです。設計の原則は次の4つです。
実際に出てきたバイト数を数えて打ち切る
展開前のファイルサイズの制限だけでは不十分で、ヘッダーに書かれた展開後のサイズは判断の根拠になりません。展開をストリームで少しずつ行い、出てきたバイト数を数え、上限を超えた時点で処理を止めます。全体をいったんメモリに展開してから大きさを確かめる実装では、確かめる前にメモリが尽きます。
件数、合計、入れ子の深さに別々の上限を設ける
アーカイブを扱う場合は、1ファイルあたりの大きさだけでなく、ファイルの件数、全ファイルの合計、入れ子の深さにも上限を設けます。業務上、アーカイブの中のアーカイブまで展開する必要がなければ、入れ子は展開しない設計にします。圧縮率(展開後の大きさを圧縮後の大きさで割った値)が極端に高いファイルを拒否する方法も、補助の判定として使えます。
展開先のパスも同時に検証する
アーカイブの中のファイル名に絶対パスや..が含まれていると、展開先の外へファイルが書き込まれます(Zip Slipと呼ばれる問題です)。Pythonのzipfileのドキュメントも、信頼できない出所のアーカイブを確認なしに展開しないよう警告しています。サイズの上限とパスの検証は、同じ展開処理の中でまとめて行います。
あわせて読みたい
パストラバーサルとは何か。基準ディレクトリの外へ解決されるパスの脆弱性と根本対策を解説
展開する処理を隔離し、資源の上限をかける
アプリケーションの上限に漏れがあっても被害を限定できるよう、展開や画像の変換は、Webの要求を受けるプロセスとは別のワーカーで行います。ワーカーにはメモリと実行時間の上限をかけ、展開用の一時領域は容量を区切った場所に置きます。1つの要求の失敗が、サービス全体の停止に広がらない構成にします。
言語とフレームワークごとの上限のかけ方
主な言語とフレームワークで、展開後の大きさに上限をかける手段を整理します。既定値と、上限が働く範囲は版によって変わることがあるため、使っている版の公式ドキュメントで確かめます。
| 対象 | 上限をかける手段 | 注意点 |
|---|---|---|
| Node.jsのzlib | gunzipSyncなどの簡易メソッドにmaxOutputLengthを指定する | 既定値はbuffer.kMaxLengthで、実質的に制限になりません。ストリームで展開する場合は自分で数えます |
| Pythonのzlib | decompressobjのdecompressにmax_lengthを指定し、少しずつ展開する | max_lengthを0にすると、入力のすべてを一度に展開します |
| Pythonのzipfile | 読み出したバイト数を数えて打ち切る | zipfileのドキュメントは、展開爆弾がディスクの枯渇を招きうると注意しています |
| ASP.NET Coreの要求展開 | 展開後の要求本文は、エンドポイントやサーバーの要求本文の上限で制限される | 要求本文の上限を無効にすると、展開後の大きさも制限されなくなります |
| Expressのbody-parser | limitオプション(既定は100kb)が、展開後の本文に対して働く | inflateの既定値はtrueで、gzip、deflate、brなどの圧縮された本文を自動で展開します |
| PythonのPillow | MAX_IMAGE_PIXELSで画素数の上限を判定する | 上限を超えると警告、2倍を超えると例外になります。Noneにすると判定が無効になります |
expressjs/body-parserのソースコードでは、Content-Encodingが付いた要求ではContent-Lengthを使わず、展開後のストリームに対してlimitの判定をしています。README自体も、既定値を大きく超える上限は、復号や変換に必要な資源の分だけメモリ使用量を増やすとして、既定値の利用を勧めています。
Node.jsでストリームの展開に上限をかける例
Node.jsのzlibのmaxOutputLengthは、gunzipSyncやbrotliDecompressのような、結果をまとめて返す簡易メソッドで働く設定です。ストリームで展開する場合は、展開後のデータが流れる途中で数を数え、上限を超えたらエラーにします。
import { createGunzip } from "node:zlib";
import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
const MAX_BYTES = 10 * 1024 * 1024; // 展開後の上限(10MB)
// 展開後のバイト数を数え、上限を超えたらパイプラインを止める
function limitBytes(max) {
let total = 0;
return new Transform({
transform(chunk, _encoding, callback) {
total += chunk.length;
if (total > max) {
callback(new Error("decompressed size limit exceeded"));
return;
}
callback(null, chunk);
},
});
}
// source と destination は、入力と出力のストリーム
await pipeline(source, createGunzip(), limitBytes(MAX_BYTES), destination);
// まとめて展開する場合は maxOutputLength を指定する。上限を超えるとエラーになる
// const data = gunzipSync(input, { maxOutputLength: MAX_BYTES });
Pythonで圧縮データとZIPに上限をかける例
Pythonのzlibでは、decompressobjのdecompressにmax_lengthを渡すと、1回の呼び出しで返す量を制限できます。処理しきれなかった入力はunconsumed_tailに残るため、上限を確かめながら少しずつ展開します。ZIPは、ファイルごとと全体のそれぞれで読み出したバイト数を数え、件数とパスもあわせて検証します。
import os
import zipfile
import zlib
CHUNK = 64 * 1024
MAX_BYTES = 10 * 1024 * 1024 # 圧縮データの展開後の上限(10MB)
MAX_ZIP_FILE = 20 * 1024 * 1024 # ZIPファイル自体の大きさの上限(20MB)
MAX_ENTRIES = 1000 # ZIPのファイル件数の上限
MAX_ENTRY_BYTES = 20 * 1024 * 1024 # ZIPの1ファイルあたりの展開後の上限(20MB)
MAX_TOTAL = 100 * 1024 * 1024 # ZIPの展開後の合計の上限(100MB)
def decompress_limited(data: bytes) -> bytes:
# MAX_WBITS | 32 で、zlib形式とgzip形式のヘッダーを自動で判別する
d = zlib.decompressobj(wbits=zlib.MAX_WBITS | 32)
out = bytearray(d.decompress(data, CHUNK))
while len(out) <= MAX_BYTES and (part := d.decompress(d.unconsumed_tail, CHUNK)):
out += part
if len(out) > MAX_BYTES:
raise ValueError("decompressed size limit exceeded")
# 途中で切れたデータと、終端の後ろに余分なデータが続く入力を拒否する
if not d.eof or d.unused_data:
raise ValueError("incomplete or trailing data")
return bytes(out)
def extract_limited(zip_path: str, dest: str) -> None:
# dest は要求ごとに新しく作り、ほかの処理が書き換えられない専用ディレクトリにする
if os.path.getsize(zip_path) > MAX_ZIP_FILE:
raise ValueError("archive too large")
dest = os.path.realpath(dest)
total = 0
with zipfile.ZipFile(zip_path) as zf:
infos = zf.infolist()
if len(infos) > MAX_ENTRIES:
raise ValueError("too many entries")
for info in infos:
# 展開先の外を指すパス(絶対パスや ..)を拒否する
target = os.path.realpath(os.path.join(dest, info.filename))
if os.path.commonpath([dest, target]) != dest:
raise ValueError("path outside destination")
if info.is_dir():
os.makedirs(target, exist_ok=True)
continue
os.makedirs(os.path.dirname(target), exist_ok=True)
# ヘッダーの申告値ではなく、実際に読み出したバイト数で判定する
written = 0
with zf.open(info) as src, open(target, "wb") as dst:
while block := src.read(CHUNK):
written += len(block)
total += len(block)
if written > MAX_ENTRY_BYTES or total > MAX_TOTAL:
raise ValueError("decompressed size limit exceeded")
dst.write(block)
zipfileは、ZipFileを開いた時点でアーカイブ内のファイル一覧をすべて読み込みます。件数の上限はその後の展開を抑えるもので、一覧の読み込み自体にかかるメモリと時間は抑えられません。そのため、ZIPファイル自体の大きさを開く前に制限し、処理全体を資源の上限をかけたワーカーで動かします。
展開先は、要求ごとにtempfile.TemporaryDirectoryなどで新しく作った専用のディレクトリにします。ほかの処理が展開先を書き換えられると、パスを検証してからファイルを開くまでの間に、ディレクトリをシンボリックリンクへ差し替えられるおそれがあるためです。上限を超えて処理を止めた場合は、途中まで書き込んだファイルを含めて、展開用の一時領域ごと削除します。例外の内容は利用者向けの画面にそのまま出さず、サイズの上限を超えたことだけを伝えます。
自動展開の設定を棚卸しする
自分で展開のコードを書いていなくても、フレームワークやHTTPクライアントが自動で展開している場合があります。次の設定を確かめます。
- サーバー側で、圧縮された要求本文を自動で展開する設定が有効か。有効なら、その上限は展開後の大きさに働くか
- HTTPクライアントが、外部から受け取った応答を自動で展開するか。展開後の応答の大きさに上限をかけているか
- 画像やOfficeファイルを解析するライブラリに、画素数やファイルの大きさの上限があるか。上限を無効にする設定が入っていないか
要求本文の自動展開は、そもそも必要かどうかから見直します。Microsoftは2026年10月に公表した.NETのBrotliの脆弱性のアドバイザリで、使っていない自動展開を新たに有効にしないよう注意し、その理由として、Brotliを除いても自動展開はサービス拒否のリスクを持ち込みうると述べています。展開の処理は、ライブラリの欠陥でも狙われます。展開処理の脆弱性がランタイムや製品へ広がった事例は、次の記事で整理しています。
あわせて読みたい
Brotliの脆弱性CVE-2026-111497とは、Node.jsや.NET、Pythonに広がる展開処理のメモリ破壊と更新先
検証は自分で管理する環境で行う
上限が働くかを確かめるには、実際に高圧縮のデータを入力して試すのが確実です。検証は、自分で管理する開発環境やテスト環境で行い、本番環境で試す場合は、管理者の許可と影響範囲の合意を得てから行います。
注意
圧縮爆弾は、相手のサーバーの資源を使い果たさせる攻撃です。他人が管理するサービスへ許可なく送りつける行為は、日本では刑法の電子計算機損壊等業務妨害罪などに触れる可能性があり、サービスの利用規約にも抵触するおそれがあります。脆弱性診断として行う場合も、対象と方法について事前に書面で許可を得ます。
テストでは、上限のわずかに下と上の大きさの入力を用意し、上限を超えた入力で処理が途中で止まること、メモリ使用量が上限の近くで頭打ちになること、一時ファイルが残らないことを確かめます。同じく入力によって処理量が爆発する正規表現の問題は、次の記事で整理しています。
あわせて読みたい
ReDoS(正規表現によるサービス拒否)の仕組みと対策。短い入力でCPUを食い潰すバックトラックを塞ぐ
確認チェックリスト
展開を扱う処理を見直す際の確認項目を整理します。
- アップロード、要求本文、外部からの応答、画像の読み込みのうち、自分のアプリケーションで展開が起きる場所を一覧にしたか
- 展開前のファイルサイズではなく、展開後に実際に出てきたバイト数で上限を判定しているか
- ZIPのヘッダーにある展開後サイズを、許可の判断の根拠にしていないか
- アーカイブ自体の大きさ、ファイル件数、1ファイルあたりと合計の展開後サイズ、入れ子の深さに上限を設けたか
- 展開先のパスを検証し、展開先の外へ書き込まれないようにしたか
- Node.jsの簡易メソッドにmaxOutputLengthを指定したか。ストリームの展開ではバイト数を数えているか
- フレームワークの要求本文の上限が、展開後の大きさに働くことを確認したか。上限を無効にしていないか
- 画像の処理ライブラリの画素数の上限を、無効にしていないか
- 展開や画像の変換を、メモリと実行時間の上限をかけた別のワーカーで行っているか
- 上限を超えた入力で処理が止まり、一時ファイルが残らないことを、自分で管理する環境で試験したか
まとめ
圧縮爆弾は、展開後の大きさを確かめない処理に、小さな圧縮データを渡して資源を使い果たさせる攻撃です。展開はZIPのアップロードだけでなく、圧縮された要求本文、外部からの応答、画像の読み込みでも起きています。
対策の中心は、展開しながら実際のバイト数を数え、上限を超えた時点で打ち切ることです。件数、合計、入れ子の深さにも上限を設け、展開先のパスもあわせて検証します。フレームワークやライブラリの上限は、既定値と働く範囲が製品ごとに違うため、使っている版の公式ドキュメントで確かめたうえで設定します。
出典・参考
- MITRE CWE-409: Improper Handling of Highly Compressed Data (Data Amplification)
- MITRE CWE-400: Uncontrolled Resource Consumption
- OWASP Cheat Sheet Series: File Upload Cheat Sheet
- zlib: Technical Details(最大圧縮率の説明)
- Python documentation: zipfile(Decompression pitfalls)
- Python documentation: zlib(Decompress.decompress の max_length)
- Node.js documentation: Zlib(maxOutputLength)
- Microsoft Learn: Request decompression in ASP.NET Core
- expressjs/body-parser(README)
- Pillow documentation: Image module(MAX_IMAGE_PIXELS)
関連する記事
ファイルアップロード機能の脆弱性と対策。Webシェルを置かせないための検証と保存の設計
ファイルアップロード機能の脆弱性(CWE-434)を、Webシェルによるリモートコード実行や保存型XSSといった悪用の機構から整理します。拡張子の許可リストに内容の実体検証を重ね、Content-Typeとファイル名を信用せず、公開ディレクトリの外へ保存してスクリプト実行を無効化する、という検証と保存の設計をWebアプリ開発者向けに解説します。
パストラバーサルとは何か。基準ディレクトリの外へ解決されるパスの脆弱性と根本対策を解説
ユーザー入力がファイルパスに連結され「../」で基準ディレクトリの外へ到達されるパストラバーサル(CWE-22)を、なぜ成立するのかという機構から、任意ファイル読取などの想定影響、正規化と許可リストによる根本対策までWebアプリ開発者向けに体系的に整理します。
XXEとは何か。XML外部実体参照で起きる情報漏えいの仕組みとDTD無効化による根本対策
XMLパーサが外部実体を解決してしまうXXE(XML外部実体参照、CWE-611)を、なぜ成立するのかという機構から、ローカルファイル読み出しやSSRF、実体展開によるDoSといった影響、DTDと外部実体の解決を無効化する根本対策までWebアプリ開発者向けに整理します。


