プロンプトとコンテキスト
ユーザーは、バックグラウンドでのウイルススキャン、テキスト抽出、またはバッチインポートのために ZIP または TAR.GZ アーカイブをアップロードします。攻撃者は、展開すると膨大なサイズになる極小の圧縮ファイル、再帰的にネストされたアーカイブ、またはターゲットディレクトリを脱出するエントリパスをアップロードする可能性があります。アップロード制限、フォーマットのパース、リソースバジェット、分離、障害状態、およびクリーンアップについて説明してください。
焦点は展開中のリソースの安全性です。アップロードサイズの制限だけでは完全な防御にはなりません。圧縮率、エントリ数、および展開後の出力も設計に組み込む必要があります。
面接官がテストするポイント
面接官は、圧縮されたバイト数と展開コストの違いを理解しているかを評価します。ファイル数、ディレクトリの深さ、ネスト、CPU、メモリ、ディスク、および実時間(wall-clock)のバジェットを含めてください。OWASP は解凍後のサイズを考慮することを推奨しており、OWASP WSTG では zip 爆弾を、ディスクまたはメモリを枯渇させてサービス拒否を引き起こすアーカイブと定義しています。
優れた回答では、トラバーサル、シンボリックリンク、重複名、虚偽のサイズ宣言、パーサーの脆弱性、およびキャンセル後のクリーンアップが網羅されます。分離されたワーカー、タスクごとのディレクトリ、アトミックな公開、およびテレメトリにより、悪意のある入力を単一の制限されたタスク内に封じ込めます。
30秒の回答
「すべてのアーカイブを信頼できないものとして扱います。エッジ側で圧縮バイト数、フォーマット、クォータ、およびリクエストレートを制限します。制限されたコンテナ内のワーカーが、エントリパス、シンボリックリンクポリシー、エントリ数、ネストの深さ、および展開後バイト数のバジェットを適用しながら、エントリをタスクごとの一時ディレクトリにストリーミングします。また、CPU、メモリ、ディスク、および実時間の制限も設けます。いずれかの制限に達した場合は展開を停止してクリーンアップします。スキャンされ、型チェックされた結果のみがアトミックに公開されます。レスポンスは安全なステータスを返し、監査イベントには生のコンテンツを含めずに理由コードと使用量を記録します。」
ステップごとの設計
ステップ 1: 入力とタスクの境界を確立する
圧縮サイズ、ユーザーごとの並行性、合計クォータ、およびリクエストレートを制限します。タスク ID、テナント、ソースオブジェクト、フォーマット、および状態を割り当てます。オリジナルは分離された実行不可能なストレージに保存します。ワーカーには短時間の読み取りアクセスのみを付与し、アップロードデータをコードや設定として扱うことは決してありません。
ステップ 2: フォーマットを特定しパーサーを選択する
拡張子や Content-Type だけを信用せず、マジックバイトと許可リストを検査します。ZIP、TAR、および GZIP 用にメンテナンスされているライブラリを選択し、そのバージョンを固定(pin)します。エントリサイズのメタデータは事前の簡易チェックに役立ちますが、それだけを唯一の根拠にすることはできません。読み取り中もバイト数のカウントを継続します。暗号化されたアーカイブ、不明な方式、および不正なヘッダーは拒否するかレビュー対象とします。
ステップ 3: 多次元のバジェットを適用する
圧縮バイト数、展開後バイト数、ファイル数、単一ファイルサイズ、ディレクトリの深さ、ネストされたアーカイブの深さ、CPU 時間、メモリ、ディスク、および実時間の制限を定義します。各タスクには厳格な上限を設けます。圧縮率は警告のトリガーにはなりますが、フォーマットやデータによって圧縮方法が異なるため、展開後バイト数のカウントの代わりにはなりません。
ステップ 4: パスとファイルシステムオブジェクトを保護する
各エントリ名を正規化し、絶対パス、トラバーサル、およびヌルバイトを拒否します。タスク固有のディレクトリに展開し、最終的なすべてのパスがその内部にとどまっていることを検証します。デフォルトでシンボリックリンク、ハードリンク、およびデバイスファイルを拒否します。上書き順序による予期せぬ問題を避けるため、重複名ポリシー(通常は拒否)を定義します。
ステップ 5: 展開とスキャンを分離する
読み取り専用ルート、一時ディスククォータ、およびネットワークアクセスなし(または最小限)を備えた低権限のコンテナまたはサンドボックス内でワーカーを実行します。1つのアーカイブが両方を密かに消費してしまわないよう、展開とスキャンに個別のバジェットを割り当てます。スキャン結果、テキスト、およびサムネイルは、ブラウザから直接ダウンロードさせるのではなく、実行不可能な新しいオブジェクトとして書き込みます。
ステップ 6: ネスト、再帰、およびキャンセルを制限する
プロダクトで不要な場合は、ネストされたアーカイブを拒否します。ネストが必要な場合は、最大深度、共有される合計バジェット、および各層でのパスチェックを設定します。再帰処理に新たなクォータを与えてはなりません。タイムアウト、キャンセル、ワーカーのクラッシュ、およびテナントの削除はすべて、一時ファイルが残らないように冪等なクリーンアップをトリガーします。
ステップ 7: 公開とリカバリにステートマシンを使用する
状態には、uploaded、inspecting、extracting、scanning、published、rejected、および cleanup_failed を設定できます。すべてのエントリがチェックとスキャンに合格した後にのみ、アトミックに公開します。安全なユーザーステータスには次のアクションを含めることができます。内部イベントには、構造化された拒否コード、バジェット使用量、およびライブラリエラーを保持します。リトライによってバジェットがスキップされたり、二重に公開されたりしてはなりません。
ステップ 8: 制限をテストして監視する
高い圧縮率、ネストされたアーカイブ、トラバーサル、シンボリックリンク、重複名、不正なヘッダー、巨大なエントリ、不正な CRC、暗号化ファイル、およびタスク途中のキャンセルをテストします。展開後バイト数、ファイル数、最大深度、CPU、拒否理由、クリーンアップレイテンシ、一時ディスク使用量の最高水準点(waterline)、およびワーカーの再起動を計測します。本番環境と全く同じライブラリバージョンに対して、合成された悪意のあるサンプルを使用します。
トレードオフ、境界、および情報の獲得
メタデータの事前チェックは低コストですが、ストリーミングカウンターの代わりにはなりません。完全なストリーミングはより安全ですが、制限に達したことを発見する前に一定のリソースを消費する可能性があります。ネストを拒否するのが最も安全ですが、ネストをサポートすればバックアップワークフローに対応できる反面、バジェットの共有や再帰的なチェックが必要になります。
ワーカーを分離するとメインサービスのリスクは低減しますが、キューのレイテンシと運用コストが増加します。オリジナルを保持することは調査やリトライに役立ちますが、保持制限、アクセス制御、および暗号化が必要になります。非同期キューが存在することは、無制限の展開を許可する理由にはなりません。
模範的な質の高い回答
「アップロードと展開を2つの信頼境界に分割します。エッジ側で圧縮バイト数、並行性、およびクォータを制限します。低権限でネットワークアクセスのないコンテナ内の非同期ワーカーが、メンテナンスされているパーサーを使用し、タスクディレクトリにのみ書き込みます。各エントリを読み取る際に、展開後バイト数とファイルをカウントし、単一ファイルサイズ、ディレクトリ深度、ネスト深度、CPU、メモリ、ディスク、および実時間のバジェットを適用します。
正規化されたパスにより、絶対パス、トラバーサル、シンボリックリンク、ハードリンク、およびデバイスファイルを拒否します。ネストが必要な場合、すべての層で1つのバジェットを共有します。結果はアトミックな公開の前にスキャンされます。制限超過、タイムアウト、キャンセル、クラッシュはいずれも冪等なクリーンアップを実行します。テレメトリはバジェット使用量、拒否理由、クリーンアップレイテンシ、ディスク使用水位を記録し、回帰テストには悪意のあるアーカイブを含めます。」
よくある間違い
- 圧縮されたアップロードサイズのみを制限する。 極小のファイルでも、展開後にディスクやメモリを枯渇させる可能性があります。
- 宣言された展開後サイズを信用する。 ヘッダーが不完全であったり、信用できなかったりする場合があります。
- 圧縮率のみを唯一のルールとして使用する。 これはシグナルの1つに過ぎず、普遍的な安全バジェットではありません。
- 共有ディレクトリに展開する。 トラバーサル、上書き、および残留物がテナントの境界を越えてしまいます。
- シンボリックリンクやデバイスファイルを許可する。 エントリによって、ターゲット外や特殊なインターフェースへ書き込みがリダイレクトされる可能性があります。
- ネストの各層に新しいクォータを与える。 再帰によりリソース消費が乗算されます。
- スキャン後に生のコンテンツをそのまま公開する。 スクリプトや誤ったコンテンツタイプによって、ダウンロード側が攻撃されるリスクが残ります。
- キャンセル後のクリーンアップをスキップする。 残留物によってディスク枯渇のインシデントが発生する可能性があります。
フォローアップの質問と回答
スキャンは展開前に実行すべきですか?
内部のファイルをスキャンするにはアーカイブを読み取る必要がありますが、展開処理自体にも分離とバジェットが必要です。まずフォーマットとメタデータのチェックを実行し、制限されたワーカーでストリーミング展開を行ってから、各結果をスキャンします。
どの圧縮率で拒否すべきですか?
フォーマット間で共通する普遍的な数値はありません。圧縮率はアラートや早期拒否のために使用し、展開後バイト数、単一ファイルサイズ、ファイル数、CPU、ディスク、実時間については厳格な制限(hard limits)を適用します。
ネストされたアーカイブを安全にサポートするにはどうすればよいですか?
最大深度を設定し、各層で1つの合計バジェットを共有し、パスとリンクのチェックを繰り返し、再帰処理を同一ワーカーの時間およびディスククォータ内に収めます。
パーサーが報告するエントリサイズは信用できますか?
それらは警告シグナルであり、認可ではありません。読み取り中もバイト数のカウントを継続し、Zip64、不正なヘッダー、データ記述子、および既知のライブラリの制限を考慮してください。
制限を超過したタスクはリトライすべきですか?
バジェット違反は決定論的な拒否であるため、無作為にリトライすべきではありません。ワーカーの一時的な障害のみをリトライ対象とし、同じバジェットと冪等な状態を持つ、分離された新しいディレクトリを作成して実行します。