わずか20MBのHTTPパケットでDjangoサーバーを1分間停止状態にできる脆弱性が公開されました(CVE-2026-33033)
(new-blog.ch4n3.kr)全体の一行要約
DjangoのMultiPartParserにおいて、Content-Transfer-Encoding: base64のパート本文が空白中心である場合に発生するPre-Auth CPU exhaustion脆弱性で、約2.5MBのリクエスト1件で通常比2,100倍以上の処理時間を引き起こします(CVE-2026-33033)
要約
- 認証なしで、デフォルト設定のサーバーでもトリガー可能
- CSRFミドルウェアがviewに入る前に
request.POSTへアクセスしてMultiPartParserを自動実行するため、認証済みエンドポイントであってもCSRF検証段階ですでに数秒を要します
- CSRFミドルウェアがviewに入る前に
- 20MBのリクエスト1件で単一workerを約1分間専有
- 4〜16個のworkerで運用する一般的なgunicorn設定であれば、同時に数十件のリクエストだけでサーバーは事実上まひします
- Djangoは
multipart/form-dataリクエストをMultiPartParserで処理し、CSRFミドルウェアがviewに入る前にrequest.POSTへアクセスするため、認証なしでもこのパーサーは常に実行されます - 脆弱性の核心は、3つのレイヤーが掛け合わさる構造にあります
- (Layer 1) base64整列while-loop: チャンクから空白を除去すると
remaining != 0の状態が維持され、field_stream.read(1)がその後のストリーム全体に対して繰り返し呼び出されます - (Layer 2)
LazyStream.read(1)の隠れたO(C)コスト:read(1)を1回呼ぶたびに、内部では約64KBのバッファ全体を取り出してから65,535バイトをunget()で押し戻すパターンが繰り返されます - (Layer 3)
unget()のO(C) bytes concatenation:bytes + self._leftoverという新しいオブジェクト生成が毎回発生します
- (Layer 1) base64整列while-loop: チャンクから空白を除去すると
- 2.5MBのリクエスト1件が内部的に約86GBのメモリコピーを誘発し、M2基準で約5.3秒間worker 1つを完全に専有します。20MBでは約1分かかります
unget()内部にはすでにsanity checkコード(_update_unget_history)が存在していましたが、今回の攻撃ではunget()サイズが呼び出しごとに1ずつ減少する単調減少パターンのため、検知条件(number_equal > 40)を決して満たしません- Djangoチームのパッチの核心は、
read(4 - remaining)→read(self._chunk_size)で、1〜3バイトずつ読む代わりに一度に64KBずつ読むよう変更した点です。これによりread呼び出しは250万回→約40回へ減少します - Nginxの
client_max_body_sizeデフォルト値は1MBですが、ファイルアップロードのエンドポイントでは緩和されることが多く、Apache httpdのLimitRequestBodyデフォルト値は1GBであるため、プロキシだけでは防御が保証されません - Claude Code + Codexを活用して発見された脆弱性であり、約20年近く磨かれてきたフレームワークにPre-Auth DoSが残っていた点が印象的です
4件のコメント
いくぞーーー
これ、実際に試してみた人いますか?
デモ用に作成されたPoCがGitHubに公開されています。
https://github.com/ch4n3-yoon/CVE-2026-33033-PoC
いいねです。