Spring BootでRequest SmugglingとRequest Splittingを取り違えない
目次
TL;DR
区別 Request Smugglingはプロキシ↔Tomcatのフレーミング差分、Request SplittingはコントローラーやフィルターへのCRLF混入。確認対象が別
smuggling確認 tomcat-embed-core 実バージョンと前段プロキシのHTTP/1.1転送設定。系列名では安全と判断せず、最新のパッチレベルに追従する
splitting確認 sendRedirect、setHeader、RestTemplate、WebClient のコード検索。リダイレクト先ドメイン許可リストとヘッダ値CRLF拒否
Spring BootのアプリでRequest SmugglingとRequest Splittingを同じCRLF系の問題として扱うと、確認箇所がずれる。
Stefanの記事では、この2つをSpring BootとTomcatの構成で分けている。
Request Smugglingは、フロントのプロキシとバックエンドのTomcatが、同じHTTP/1.1バイト列を違う境界で読む問題だ。
Content-Length と Transfer-Encoding の扱いがずれると、プロキシがボディだと思って流したバイト列を、バックエンドが次のリクエストとして処理する。
Request Splittingは、アプリケーションが作るヘッダ値やリダイレクト先にCRLF(キャリッジリターン+ラインフィード、\r\n。HTTPで行の区切りに使う制御文字)が混ざり、レスポンスヘッダや転送先リクエストが分割される問題だ。OWASPの正式名称はレスポンス側を指す「HTTP Response Splitting」で、この記事ではそれに加えて転送先リクエストへのCRLF混入も含めてRequest Splittingと呼んでいる。
HttpServletResponse#sendRedirect、setHeader、Springの HttpHeaders、RestTemplate や WebClient でのヘッダ転送が関係する。
名前が似ていて両方ともCRLFやHTTPヘッダが絡むため、確認手順を混ぜてしまいやすい。発生レイヤ・確認対象・対策の3点で対比すると、別の作業だとわかる。
| 観点 | Request Smuggling | Request Splitting |
|---|---|---|
| 発生レイヤ | プロキシ↔Tomcat間のHTTP/1.1メッセージ境界 | アプリのヘッダ値・リダイレクト先の文字列処理 |
| 原因 | フロントとバックエンドのフレーミング解釈差 | 信頼できない入力中のCRLFがヘッダ終端として通る |
| 確認対象 | tomcat-embed-core のバージョン、前段プロキシの転送設定 | sendRedirect、setHeader、HTTPクライアント転送コード |
| 主な対策 | プロキシで曖昧リクエストを拒否、HTTP/2でホップを通す | リダイレクト先の許可リスト、ヘッダ値のCRLF拒否 |
| Spring Bootの主な触り所 | 依存ツリーと前段プロキシ設定 | コントローラー・フィルター・クライアント呼び出し |
フレーミング差分がどこで生まれるかを図にすると、Smugglingが2台のサーバーの解釈差で起きることがはっきりする。
flowchart TD
A[クライアント] -->|CLとTEを同居させた<br/>HTTP/1.1リクエスト| B[フロントプロキシ<br/>nginx HAProxy ALB]
B -->|片方のヘッダで<br/>境界を決めて転送| C[バックエンド<br/>組み込みTomcat]
C -->|別のヘッダで<br/>境界を決めて解釈| D[境界がずれる]
D --> E[残りバイト列が<br/>次リクエストの先頭になる]
E --> F[後続ユーザーの<br/>リクエストへ前置注入]
smugglingはプロキシとTomcatの境界で起きる
Spring BootのServletスタックは、標準構成だと組み込みTomcatを使う。
Spring Bootの公式ドキュメントでも、spring-boot-starter-web はTomcatを含むと説明している。
Tomcat単体で受けるリクエストだけを試しても、Request Smugglingの判定にはならない。
攻撃が成立するのは、nginx、HAProxy、AWS ALBのような前段と、Tomcat側でHTTP/1.1のメッセージ境界が一致しないときだ。
バリアントは、フロントとバックエンドがメッセージ境界をどのヘッダで決めるかの組み合わせで分類する。
HTTP/1.1ではボディ長の決め方が2系統ある。Content-Length でバイト数を指定する方法と、Transfer-Encoding: chunked でチャンクを並べる方法だ。
この2つが1リクエストに同居し、フロントとバックエンドが別々に解釈すると境界がずれる。
| バリアント | フロント側の解釈 | バックエンド側の解釈 | 成立する差分 |
|---|---|---|---|
| CL.TE | Content-Length でボディ長を読む | Transfer-Encoding: chunked として読む | フロントが Content-Length 優先、バックエンドが Transfer-Encoding 優先 |
| TE.CL | Transfer-Encoding: chunked として読む | Content-Length でボディ長を読む | フロントが Transfer-Encoding 優先、バックエンドが Content-Length 優先 |
| TE.TE | 両方とも Transfer-Encoding を読むが、片方だけ無視させられる | 同上(無視される側が逆) | 不正な書き方(Transfer-Encoding: xchunked 等)で片方だけパースを失敗させる |
CL.TEとTE.CLは、フロントとバックエンドが優先するヘッダ自体が逆になっているケースだ。
TE.TEは、両方とも Transfer-Encoding を見るが、Transfer-Encoding:[Tab]chunked のような不正な記法でどちらか一方にだけ解釈を失敗させ、結果的にCL.TEかTE.CL相当の境界ずれを作る。
RFC 7230 Section 3.3.3(現在はRFC 9112 Section 6.1が後継)は、Transfer-Encoding と Content-Length が同時にあるメッセージをrequest smugglingやresponse splittingの兆候として扱い、転送前に Content-Length を取り除くべきだとしている。
対策の力点は「どちらのヘッダを優先するか」を決めることではなく、曖昧なフレーミングを下流へ流さないことに置く。
Tomcat 9系では、9.0.31でRequest Smuggling関連の修正が入った(影響は9.0.30以前)。
Tomcatのセキュリティページによると、9.0.0.M1から9.0.30の範囲で無効なHTTPヘッダが有効と解釈される問題(CVE-2020-1935)があり、特定のリバースプロキシ背後ではRequest Smugglingにつながる。
9.0.28から9.0.30のリグレッション(CVE-2019-17569)でも、無効な Transfer-Encoding ヘッダ処理から同種の問題が起きた。
どちらも9.0.31で修正されている。
この段階の確認対象はコントローラーではない。
pom.xml や build.gradle で実際に入っている tomcat-embed-core、前段プロキシのHTTP/1.1転送設定、keep-alive、ヘッダ正規化、バックエンドへHTTP/2でつなげるかどうかを洗う。
splittingはコントローラーやフィルターの文字列処理で起きる
Request Splittingは層が違う。
攻撃者の入力がヘッダ値に混ざると、CRLFがヘッダ行の終端として解釈される。
OWASPのHTTP Response Splittingページは、信頼できない入力が検証されずにHTTPレスポンスヘッダへ入ることで、攻撃者が残りのヘッダやボディを操作できると説明している。
CRLF Injectionページでも、HTTPではCRLFが行終端として使われるため、HTTP response splittingのような問題につながると書いている。
Spring MVCだと、リダイレクト処理で出やすい。
@GetMapping("/redirect")
public void redirect(
@RequestParam String redirectUrl,
HttpServletResponse response
) throws IOException {
response.sendRedirect(redirectUrl);
}
redirectUrl に %0d%0aSet-Cookie:%20session=attacker のような値が入り、それがヘッダ値として通ると、Location の次に攻撃者指定の Set-Cookie が並ぶ。
最近のServletコンテナはCRLF入りのヘッダ値を拒否する経路を持つ。
ただし、全ての危険な文字列がコンテナの検証を通るとは限らない。
フィルターで X-Forwarded-Host を読み、RestTemplate で内部APIへそのまま転送するようなコードでは、レスポンスヘッダではなく「次に送るリクエスト」のヘッダ構築が発火点になる。
この点は、以前書いたaxiosのprototype pollution gadgetとCRLFヘッダ注入に近い。
あの記事ではNode.js側のHTTPクライアントが汚染値をヘッダに乗せる話だった。今回のSpring Boot側で見るのは、Javaのコントローラー、フィルター、HTTPクライアント転送コードがCRLFをヘッダ値へ流すかどうかだ。
ダウングレードホップにHTTP/1.1の差分が残る
HTTP/2はバイナリフレーミングなので、HTTP/1.1の Content-Length と Transfer-Encoding の食い違いはそのままでは出ない。
プロキシからTomcatまでHTTP/2で通せるなら、古典的なCL.TEやTE.CLの経路は消える。
ただし、クライアントからプロキシまではHTTP/2、プロキシからバックエンドはHTTP/1.1という構成は普通にある。
PortSwiggerのHTTP/2 request splitting labが扱うのは、フロントがHTTP/2をHTTP/1.1へダウングレードする構成だ。
ヘッダを十分にサニタイズしないと、CRLFでリクエストが分割される。
Spring Boot側だけをHTTP/2対応にしても、途中でHTTP/1.1へ戻るホップがあればフレーミング差分は残る。
同じHTTP/2でも、HTTP/2 Bombの記事で扱った話とは別だ。
あちらはHPACK、cookie crumb、flow controlでサーバーのメモリを保持させるDoSだった。
今回のsmuggling/splittingは、ヘッダ圧縮の増幅ではなく、リクエストやレスポンスの境界をプロキシとバックエンドが違う位置で切ることで起きる。
Spring Bootで洗うファイルと設定
2026年時点の現行はSpring Boot 4系で、既定の組み込みTomcatは11系になる。
ただし古いSpring Boot 2/3系を使っている場合、BOMに任せていても実際のTomcatが古いまま固定されていることがある。
mvn dependency:tree や gradle dependencies で tomcat-embed-core の実バージョンを出す。
mvn dependency:tree | grep tomcat-embed-core
./gradlew dependencies --configuration runtimeClasspath | grep tomcat-embed-core
ここで「9.0.31以降だから安全」「10.1系だから既知修正済み」と系列名で判断しないこと。
Request Smugglingはその後も定期的に見つかっており、2026年にもchunk extension処理のCVE-2026-24880が9.0/10.1/11.0系の広い範囲(9.0.115、10.1.52、11.0.18まで)に影響している。
安全かどうかはマイナーバージョン(パッチレベル)でしか言えないので、系列ではなく実バージョンを最新に追従させる。
あわせて、前段プロキシが曖昧なリクエストを正規化してからバックエンドへ流す構成では、Tomcat側に元の異常が届かない点も確認する。
プロキシで拒否する、バックエンドへ流すホップをHTTP/2にする、Content-Length と Transfer-Encoding の同居をテスト環境で400にできるか確認する。
不正ヘッダを拒否するかどうかはTomcatコネクタの rejectIllegalHeader 属性で決まる。
古いTomcat(8.5系)ではデフォルトが false で、不正な Content-Length を拒否しないまま通していた。CVE-2022-42252はこの設定が false のときにリバースプロキシ背後でsmugglingが成立する問題で、新しいTomcatではデフォルトが true に変わっている。true だと不正ヘッダを含むリクエストを400で落とす。
Spring Boot側の server.tomcat.reject-illegal-header プロパティは2.7.12で非推奨化され、3.4で削除された(Boot 4系には存在しない)。古いバージョンでこれを明示的に false にして無効化していないかを設定ファイルで確認する。
ヘッダ長の上限は server.max-http-request-header-size(Spring Boot 3系。2系は server.max-http-header-size)で、巨大ヘッダによる別系統の問題と合わせて見ておく。
Splitting側は、コード検索から入る。
rg "sendRedirect|setHeader|addHeader|HttpHeaders|RestTemplate|WebClient" src
リダイレクト先はドメイン許可リストで絞る。
ヘッダ値に使う文字列は、\r、\n、\x00 を拒否する。
受信ヘッダを上流へ転送するコードは、全部渡すのではなく、必要なヘッダ名だけを明示してコピーする。
参考
- Request Smuggling vs Request Splitting in Spring Boot
- Apache Tomcat 9 vulnerabilities
- Apache Tomcat 10.1 HTTP Connector (rejectIllegalHeader)
- Spring Boot Embedded Web Servers
- RFC 7230 Section 3.3.3
- RFC 9112 Section 6.1
- OWASP HTTP Response Splitting
- OWASP CRLF Injection
- PortSwigger HTTP/2 request splitting via CRLF injection