技術者ブログ
クラウド型WAF「Scutum(スキュータム)」の開発者/エンジニアによるブログです。
金床“Kanatoko”をはじめとする株式会社ビットフォレストの技術チームが、“WAFを支える技術”をテーマに幅広く、不定期に更新中!

Apache TomcatのOmitted ACK DoS攻撃の脆弱性(CVE-2025-53506)の解説
はじめに
最近「HTTP/2チョットデキル」な雰囲気の金床です。今回は私が発見し報告したApache TomcatのDoSの脆弱性であるCVE-2025-53506について解説します。私はウェブセキュリティの専門家ですが、脆弱性を見つけるというよりは守る・検知する側の研究と実装を主な活動としており、このようなCVE番号を割り振られるような脆弱性を発見することはほとんどありません。久しぶりにCVEを取得したため、そういえば最後に自分がCVEを取ったのっていつだっけ…と調べたところ、なんと2006年のMySQLのCVE-2006-3081でした。実に19年ぶりです。Time fliesですね。
脆弱性の深刻度
CVE-2025-53506はHTTP/2プロトコルにおいて、攻撃者が工夫することによりTomcatが備えている接続数の並列度管理をかいくぐり、大量のリクエスト送信によって負荷を高めるものです。結果として攻撃者が得るのはDoSであり、サービスがダウンします。任意のコード実行や情報漏洩は発生しません。
このような「ソフトウェア的なバグに起因するDoS脆弱性」というのはHashDoS等たくさんのものが知られていますが、実は現実的にはあまり攻撃されません。というのは、攻撃者はウェブサービスやウェブサイトをダウンさせたい場合、手っ取り早く分散型のF5攻撃的なリクエスト大量送信やSyn Flood、Reflection Attackなどを行うからです。攻撃者はわざわざ「今回の攻撃対象はTomcatでこのバージョンでこの脆弱性があると思われるので、この部分を調整してDoSを引き起こせるように…」みたいな手間をかけるケースはとても少ないです。多くの場合、前述したような多くのソフトウェアに共通して効果が期待される、よく知られた攻撃をしてきます。
例外としてHTTP/2のRapid Reset Attackなどは実際にCloudflareなどに行われたため、もちろんCVE-2025-53506についても攻撃が来る可能性はありますが、即悪用されるという可能性はやや低めの脆弱性かなと予想しています。いずれにせよ、可能な限り早くアップデートを実施しましょう。
Scutumで防御可能
Scutumを導入している場合、CVE-2025-53506について防御可能です。ネットワークの構成上、Scutumを導入している場合には攻撃者はTomcatと直接HTTP/2で通信することができないからです。
Omitted ACKとは
ここから技術的な解説に入ります。今回の脆弱性を引き起こすテクニックはOmitted ACK(あるいはOmitted SETTINGS ACK)です。HTTP/2プロトコルにおいては、コネクション確立直後にクライアントとサーバの間でお互いにSETTINGSフレームが送信されます。SETTINGSフレームはさまざまなパラメータを通知するもので、例えば「私はこのくらいの並列度で通信することを要望します」のような内容を含みます。特にサーバ側としては1つのクライアントがあまりにも大量のリクエストを送信してくると困るため、普通は100くらいの並列度を要求します。Apache httpdやTomcatは100、NGINXは128などの数値をクライアントに伝えます(いずれもデフォルト値)。
SETTINGSフレームを受信した側(ここでの文脈ではクライアント)は、その内容を了解し、今後の通信に適用する意思を伝えるためにACKフラグを立てたSETTINGSフレームを返信しなければいけません。また、SETTINGSフレームを送った側(ここではサーバ)は、このACKフラグの立った返信を受信した時点から、自分が最初のSETTINGSフレームで伝えた内容を実際の通信に適用できるという決まりになっています。これはRFC9113のここで定義されています。
鋭い人はここまで読んだ時点で雲行きが怪しくなってきたと感じるのではないでしょうか。そう、サーバはコネクション確立直後に「並列度100で通信するぞ!」と伝えるのですが、実際にその100という数値を適用することが許されるのは、クライアントがACKの返信をした時点以降とされているのです。つまりクライアントはACKを返信しないことで、並列度が100にされるのを実質的に拒否することがHTTP/2プロトコル上可能なのです。では、この「並列度は100がいい」というサーバの要望に返事をしない間は、この並列度の値は何になるのでしょうか。
RFC上の並列度のデフォルト値は無限大
並列度(SETTINGS_MAX_CONCURRENT_STREAMS)、つまり「処理中のストリームをいくつ生成してよいか」のデフォルト値はRFC9113上では無限大(制限なし)とされています。無限大です。unlimitedです。
「え、いくつでもストリームを作ってもいいのか?」
「ああ…しっかり作れ…」
「HEADERS、HEADERS、HEADERS」
「おかわりもいいぞ!」
Tomcatはこの点を律儀に守っており、SETTINGSフレームとそれに対するACKで合意が形成される時点よりも前までは並列度としてここで定義されている 4294967296 という非常に大きな値が使われます。私の個人的な見解として、このRFC上のデフォルト値がunlimitedというのは完全におかしいと考えており、100〜500程度の値とするべきだったと思います。
SETTINGSを待たずにリクエストできる
HTTP/2プロトコルの面白いところとして、コネクション確立直後にクライアントとサーバがお互いに「今回の通信はこんな感じのパラメータでいきましょう」とお互いにSETTINGSフレームを送ることが必須とされている一方で、素早いウェブページの描画を実現するためサーバからのSETTINGSフレームの受信や処理に先んじてクライアントはリクエスト(HEADERSフレーム等)の送信が許されている点があります。このためコネクション確立直後にRFCで定められた決まりに則りながら大量のHEADERSを送信してよいのです。もしHTTP/2が「まず最初にSETTINGSフレームをお互い送信し、それを確認してACKを返信する。続いて、リクエストとレスポンスのやりとりをはじめる」というわかりやすい順番を定める内容であれば、今回のような脆弱性が入り込むスキはありませんでした。しかしそれでは最初のリクエストのレイテンシがHTTP/1.1よりも大きくなってしまうため、当然そのような内容にはできなかったのでしょう。
遅いACKに対するタイムアウト
RFC上、クライアントもサーバも、自身が送ったSETTINGSに対するACKの返信がいつまでたっても来ない場合はおかしいので、ある程度の時間待った上で「ACKが来ない」ことを理由にエラーとしコネクションを閉じることが許可されています。Tomcatはこれを実装していませんでした。これが今回の脆弱性を生んでしまった原因となっています。この「ACKが来ないときのタイムアウトエラー」はRFC上ではMAYとされていることもあり、Apache(nghttp2)やNGINX等の他のHTTP/2実装でもあまり積極的には実装されていないように見えます。
なぜDoSになるか
攻撃者はこのように並列度の制限を回避することに成功し大量のHEADERSフレームの送信ができますが、それと実際にDoSになるかはまた別の問題です。例えば、サーバ側の実装が極端にシンプルであるケースを考えてみます。
- HEADERSフレーム(リクエスト)をTCP(TLS)ストリームから1つ読み込む。
- JSPやサーブレットの処理、あるいは画像やCSSファイルを読んでレスポンスを生成する
- ストリームにレスポンスを書き込む
- 次のリクエストを読む
この場合はサーバ側は単に1つのスレッドが忙しくなるだけで、サーバ全体が処理が重くなるようなことはありません。大量に送信されるHEADERSフレームの内容も単なるTCPレイヤーのデータとしてカーネル内などに一定の量溜まるだけで問題を起こさないでしょう。しかしこのようなシンプルすぎるシーケンシャルな実装では、例えば途中の1つのサーブレットの処理が重い場合などに後続のすべてのリクエストを待たせることになるため、ウェブサイトの表示速度が著しく悪化するでしょう。また、I/Oの時間もすべてのレスポンス時間に積み重なって影響してしまいます。そのため、一般的なウェブサーバやアプリケーションサーバは次々に到着するリクエスト群に対して複数のスレッドを割り当て、並列的に処理を行います。
Tomcatでは1つのHTTP/2コネクションに対して最大で20個のワーカスレッドを割り当てるようです。そのため攻撃者が大量のHEADERSを送信すると、この20個のスレッドはフル稼働することになるでしょう。さらに次々と到着するHEADERSフレームをキューに入れて管理する部分ではデータ構造に対して大量の追加と削除が発生するので、その部分でもCPUやメモリなどのリソースを消費していきます。これらが合わさり、最終的にサービスが提供できない、いわゆるDoSの状態となってしまうようです。Tomcatではそもそもこのような大量のリクエスト到着がDoSにならないようにするために並列度の最大値が内部的に利用されているのですが、それが回避されてしまうためにDoSになってしまいます。
どのように修正されたか
Tomcatはこの脆弱性への対応として、シンプルにSETTINGSフレーム送信時に、ACKを待つことなく最初から内部的な制限値を適用するというアプローチで修正を行いました。これはRFCには違反していますが、非常に明快かつ効果的です。行儀の良いクライアントはすぐにACKを返信するため、実用上はまったく問題ありません。現時点ではTomcat(10.1系)の最初のSETTINGSフレームの内容はSETTINGS_MAX_CONCURRENT_STREAMS項目だけであるため、ウィンドウ管理等についてはRFCで定義されている初期値がクライアントとサーバ双方で使われるため、その部分の調整についての不整合は発生しません。
RFCでMAYで定義されているACK不着時のタイムアウトは実装がやや複雑になりますし、タイムアウト値を短くしないと、結局初期に攻撃が入り込むスキが残ってしまいます。一方、タイムアウト値が短いと正常なクライアントとの通信で誤ってエラー判定してしまう可能性が上がってしまうので、現実的にはなかなか厄介な実装だと言えるでしょう。
まとめ
今回はTomcatのHTTP/2処理に存在したDoSの脆弱性について技術的な解説を行いました。HTTP/2は非常に複雑であるため、いかにもこのような脆弱性が入り込みそうな印象を前から持っていましたが、実際に発見するという形になりました。報告後すぐに対応してくれたTomcat開発チームにお礼を申し上げます。この記事についての感想やツッコミなどありましたら@kinyukaまで。