Spring Security は HTTP レスポンスのセキュリティヘッダーを黙って破棄する。デモ/教育目的のみ。このローカルアプリ以外に対して実行しないこと。
| CVE | CVE-2026-22732 (CWE-425)、公開 2026-03-19 |
| CVSS 3.1 | 9.1 CRITICAL — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N |
| 直接依存 | spring-boot-starter-web + spring-boot-starter-security 2.7.18 |
| 脆弱なコンポーネント | spring-security-web / -config / -core 5.7.11 — 推移的のみ、pom.xml には一切記載なし |
| 修正済みコンポーネント | spring-security-web 5.7.14-0.cgr.2、1 箇所の <version> 変更で到達 — 遷移を参照 |
| 検証環境 | Tomcat 9.0.118、JDK 17.0.18、macOS arm64 |
影響範囲: 5.7.0–5.7.21、5.8.0–5.8.23、6.3.0–6.3.14、6.4.0–6.4.14、6.5.0–6.5.8、7.0.0–7.0.3。 Spring Boot 2.7.18 は Spring Security 5.7.11 を固定しており、最初の範囲のまさに中にある:
$ mvn dependency:tree -Dincludes=org.springframework.security
+- org.springframework.security:spring-security-config:jar:5.7.11:compile
| \- org.springframework.security:spring-security-core:jar:5.7.11:compile
\- org.springframework.security:spring-security-web:jar:5.7.11:compile
./run.sh # terminal 1 — builds and starts on :8080 (pins JDK 17)
./exploit.sh # terminal 2 — drives every endpoint, diffs the headers
run.sh は JAVA_HOME を固定している。Spring Boot 2.7.x は、このマシンで mvn がデフォルトで解決する JDK 25 では動作しないためである。JAVA_HOME_17=/path/to/jdk17 で上書きできる。
また、デフォルトで -s settings-chainguard.xml を付けてビルドする。修正済みの親 POM が Maven Central に存在しないためである。別の場所を指定するには MAVEN_SETTINGS=/path/to/your/settings.xml を設定するか、MAVEN_SETTINGS= で Central のみからビルドする — これは素の 2.7.18 でのみ動作する。
exploit.sh は実際の spring-security-web と spring-boot のバージョンを target/*.jar から読み取るため、そのバナーは常にハードコードされた文字列ではなく、実際に動作しているものを報告する。
SecurityConfig はヘッダーのカスタマイズを一切行わない — Spring Security のデフォルトが有効であり、これはまさにセキュリティを意識したアプリが依存するものである。すべてのエンドポイントは同じ機密性の高いボディを返す:
{"account":"4111-1111-1111-1111","holder":"D. Havelock","balance":"82914.55"}
異なるのは、コントローラーがレスポンスをどのように書き込むかだけである。
BASELINE standard Spring MVC return value
/safe/account OK all 6 headers delivered
CONTROL getOutputStream(), body > 8 KB buffer
/vuln/stream/account OK all 6 headers delivered
CONTROL explicit response.flushBuffer()
/vuln/flush/account OK all 6 headers delivered
EXPLOIT setIntHeader("Content-Length", n) <-- CVE-2026-22732
/vuln/content-length/account BYPASSED ALL 6 security headers dropped
BY DESIGN application sets its own Expires header (NOT this CVE)
/vuln/cache/account PARTIAL Cache-Control + Pragma dropped
CVE が修正されたときに状態が変わるのは /vuln/content-length/account だけであり、exploit.sh が判定を導き出すのはこのエンドポイントのみである。残りは対照群である。
setIntHeader("Content-Length", n) → 完全バイパス一見普通のコントローラーコード 3 行が、Spring Security が約束したすべてのヘッダーを剥ぎ取る:
response.setContentType("application/json");
response.setIntHeader("Content-Length", body.length);
response.getOutputStream().write(body);
$ curl -sD - -o /dev/null http://localhost:8080/vuln/content-length/account
HTTP/1.1 200
Content-Type: application/json
Content-Length: 77
Date: Tue, 01 Sep 2026 00:53:18 GMT
X-Frame-Options なし、X-Content-Type-Options なし、Cache-Control なし、Pragma なし、Expires なし、X-XSS-Protection なし。6 つすべてを含む /safe/account と比較せよ。レスポンスは任意のオリジンからフレーム化可能で、MIME スニッフィング可能で、キャッシュ可能 — しかもカード番号を配信しながらである。
訂正: この README の以前のバージョンではこれを「exploit 2」と呼び、アドバイザリが文書化している条件であると主張していた。これは CVE-2026-22732 の一部ではなく、アップグレードしても修正されない。CacheControlHeadersWriter は 5.7.11、5.7.14-0.cgr.2、6.5.8(最後の脆弱版)、6.5.9(最初の修正版)でバイト単位で同一である — ソース jar を diff して検証済み。その Javadoc はこの挙動を明言している: 「キャッシュ制御ヘッダーが指定されていない場合に、キャッシュを防ぐヘッダーを挿入する。」
それでもこれを実証する価値はある。リークは現実であり、残存リスクはパッチ適用後も生き残るからである。CacheControlHeadersWriter は Cache-Control、Expires または Pragma がすでに存在する場合に処理を中止するため、3 つのうちどれか 1 つを設定するだけで Spring Security の no-store ディレクティブすべてが抑制される。善意の 1 行でそれが起きる:
response.setHeader("Expires", "Thu, 01 Jan 2099 00:00:00 GMT");
/safe/account NOT cacheable (Cache-Control: no-cache, no-store, max-age=0, must-revalidate)
/vuln/cache/account CACHEABLE (Cache-Control: absent / Expires: Thu, 01 Jan 2099 00:00:00 GMT)
/vuln/content-length/account CACHEABLE (Cache-Control: absent / Expires absent)
上の表では Expires は「存在する」とみなされるが、アプリが選んだ攻撃者に都合の良い値によって — Spring Security の Expires: 0 は単に破棄されたのではなく置き換えられた。カード会員データは今や、経路上のすべてのブラウザと共有プロキシによって 2099 年まで保存可能である。
修正済みビルドでは /vuln/content-length/account は NOT cacheable に反転するが、/vuln/cache/account は上記のまま変わらない。これを修正するのはアプリケーションコードかリバースプロキシのみである — これはデモで声に出して言うべき有用なことである: ライブラリをアップグレードすると CVE は解消されるが、これは手つかずのまま残る。
広く出回っているいくつかの記事 — 公開された再現リポジトリを含む — は response.getOutputStream() と response.flushBuffer() をトリガーとして挙げ、「Spring Security がヘッダーを注入する前にレスポンスがコミットされる」と説明している。Spring Security 5.7.11 ではそれは誤りである。 両エンドポイントとも 6 つのヘッダーすべてを配信する。
/diag/committed はその説明が成り立たない理由を示す。12 KB の書き込み後、レスポンスは実際にコントローラー内でコミットされているが、それでもヘッダーは到着する:
>>> DIAG response.isCommitted() after 12048 byte write = true
| wrapper class = org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse
OnCommittedResponseWrapper は flushBuffer() と出力ストリームの書き込みをオーバーライドするため、それらのコミットより先にヘッダーを出せる。コミット順序だけがバグではない; 宣言された Content-Length の経路がそうである。Spring のアドバイザリ自体、コミット順序の話を一切支持していない。
これら 2 つのエンドポイントを残すことで PoC は反証可能になる: 何が再現しないかを、何が再現するかと同じくらい明確に示し、両方ともパッチを通じてグリーンのままであることが、反転する唯一のエンドポイントを意味あるものにしている。
pom.xml の 1 行、それだけ。 ソース変更なし、プロパティ変更なし、Spring Boot のメジャーアップなし:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version> <!-- vulnerable -->
<version>2.7.18-0.cgr.3</version> <!-- patched -->
</parent>
これにより親の spring-boot-dependencies を通じて spring-security.version が 5.7.11 から 5.7.14-0.cgr.2 に(そして spring-framework.version が 5.3.31 から 5.3.39-0.cgr.4 に)再固定される。測定結果:
spring-security-web のソース jar を diff すると、重要なファイルは 1 つだけである。5.7.11 → 5.7.14-0.cgr.2 は OnCommittedResponseWrapper に setHeader / setIntHeader / addIntHeader のオーバーライドを追加し、それぞれが Content-Length を setContentLength() 経由でルーティングする:
@Override
public void setIntHeader(String name, int value) {
checkContentLengthHeader(name, value); // <-- added
super.setIntHeader(name, value);
}
修正前は addHeader だけがこれを行っていたため、setIntHeader("Content-Length", n) はラッパーの追跡長を 0 のままにし、onResponseCommitted() は決して発火せず、HeaderWriterFilter は Tomcat がレスポンスをコミットする前にヘッダーを書かなかった。
アップストリームの 6.5.8(最後の脆弱版)と 6.5.9(最初の修正版)を diff しても同じハンクがそのまま現れるため、これは再実装ではなく公式修正のバックポートである。Chainguard ビルドはアップストリーム 6.5.9 にはない 2 つの null ガードを追加している(String オーバーロードの value != null、および append の (csq != null) ? csq.length() : 4)。
Spring Security 5.7.x はアップストリームで EOL である; OSS の修正は 6.4.15 / 6.5.9 / 7.0.4+ にのみ入る。再ビルドされた 5.7.x が選択肢にならない場合:
ObjectPostProcessor 経由で HeaderWriterFilter.shouldWriteHeadersEagerly = true を設定する。アドバイザリによればこれは挙動を変える: アプリケーションが書いたヘッダーは、Spring Security のヘッダーを抑制するのではなく、特定のヘッダーのみを上書きするようになる。これはまた /vuln/cache/account も修正するが、バージョンアップではそれは修正されない。これらはいずれもこのプロジェクトには組み込まれていないため、脆弱な挙動がデフォルトであり、修正済み状態は上記の単一の <version> 変更で到達可能である。
Boot 2.7.18 は Tomcat 9.0.83 を固定しており、grype . はこれを 34 件の CVE(4 件 Critical) で指摘する。それらはすべて 9.0.118 以下で修正されており、9.0.118 は最新の 9.0.x リリースである — つまり 1 つのプロパティでこのセットが解消される:
<properties>
<tomcat.version>9.0.118</tomcat.version>
</properties>
spring-boot-dependencies はすべての tomcat-embed-* アーティファクトをこの単一のプロパティ経由で宣言しているため、これを上書きすると core、el、websocket がまとめて再固定される。検証済み:
$ mvn dependency:tree | grep tomcat-embed
tomcat-embed-core:jar:9.0.118:compile
tomcat-embed-el:jar:9.0.118:compile
tomcat-embed-websocket:jar:9.0.118:compile
制約: 9.0.x 系に留まること。Tomcat 10+ は Servlet API を jakarta.* に移行したが、Spring Framework 5.3 は javax.servlet に対してコンパイルされるため、10.x/11.x へのアップは実行時にサーブレット型で NoClassDefFoundError を起こして失敗する。
同じ仕組みを Boot 2.7.18 の管理依存関係の残りに適用する。メジャーバージョンアップなし、Spring Boot アップグレードなし:
これらのオーバーライドは修正済みの親 POM と相互作用するため、デモ前にその動作を把握しておくこと。 2.7.18-0.cgr.3 では親がすでに tomcat.version 9.0.118、logback.version 1.2.13、snakeyaml.version 1.33 を提供している — これら 3 行は完全な重複となり、何も変えずに削除できる。jackson-bom.version と log4j2.version の行は依然として実際に機能する: 修正済みの親は Boot の標準 2.13.5 / 2.17.2 を保持するため、オーバーライドが優先され、これら 2 つの依存関係は Chainguard ビルドではなく素のアップストリームビルドに解決される。spring-framework.version は意図的にコメントアウトされており、これにより親の 5.3.39-0.cgr.4 が通る。
grype . の推移| 状態 | 検出数 | 内訳 |
|---|---|---|
| 素の Boot 2.7.18 | 99 | 7C / 39H / 38M / 15L |
| + Tomcat アップ | 65 | 3C / 23H / 29M / 10L |
| + メジャー内アップすべて |
完全に解消: Tomcat (34)、Jackson (7)、log4j (1)。全体で 99 → 43、High は 39 → 12。
各アップ後に検証済み: アプリは Tomcat/9.0.118 で起動し、6 つのエンドポイントすべてが 200 を返し、CVE はバイト単位で再現する。Spring Boot は依然 2.7.18、Spring Security は依然 5.7.11 であるため、CVE-2026-22732 は手つかずである — これがこのセクションの要点であり、同時にその正直な限界でもある: 周囲のすべてにパッチを当てても、アプリケーションフレームワークの CVE には何の効果もない。それを修正するにはプロパティではなく親 POM のアップが必要である。
最新の 1.x は 1.6.3 である — 同じメジャーなので名目上は範囲内である。しかし動作しない。Logback 1.3+ は SLF4J 1.7 の StaticLoggerBinder を SLF4J 2.x の ServiceLoader プロバイダに置き換えたが、Boot 2.7 の LogbackLoggingSystem は StaticLoggerBinder を直接呼び出す。1.5.38 での測定:
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder"
Caused by: java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder
at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(...:304)
これを回避するには SLF4J 2.x(メジャーアップ)および Boot 3.x のロギングシステムが必要である。1.2.13 が真の上限であり、この系では 6 件の Logback 検出(Medium 2、Low 4)が修正不能のまま残る。
| コンポーネント |
|---|
残る 3 件の Critical のうち 2 件は、スコアではなくきちんと読む価値がある:
HttpInvokerServiceExporter 経由のデシリアライズ。このアプリは HTTP Invoker を使用しないため、ここでは到達不能である。5.7.14-0.cgr.2 が修正バージョンを超えているため、副作用としてこれを解消する。残存分は構造的である: Spring Framework 5.3.x と Spring Security 5.7.x はどちらも EOL である。Tomcat や Jackson ではなく、これが Boot 3.x 移行の本当の論拠である。
pom.xml parent + 2 starters, nothing else. Flip the <version> to switch state.
run.sh build + run on JDK 17, via settings-chainguard.xml
exploit.sh header diff, cache impact, clickjacking check, CVE-scoped verdict
settings-chainguard.xml Chainguard Libraries repo -- required for the patched parent
src/main/java/com/example/poc/
PocApplication.java @SpringBootApplication
SecurityConfig.java permitAll, zero header customisation
AccountController.java baseline, 2 exploits, 2 controls, 1 diagnostic
認証は permitAll、CSRF はオフで curl が未認証で動作するようにしている — どちらもこの CVE の一部ではない。
| 2.7.18 | 2.7.18-0.cgr.3 |
|---|
/safe/account | OK 6/6 | OK 6/6 |
/vuln/stream/account | OK 6/6 | OK 6/6 |
/vuln/flush/account | OK 6/6 | OK 6/6 |
/vuln/content-length/account | BYPASSED 0/6 | OK 6/6 |
/vuln/cache/account | PARTIAL 4/6 | PARTIAL 4/6 (by design) |
exploit.sh verdict | VULNERABLE | PATCHED |
| プロパティ | Boot 2.7.18 デフォルト | ここでの固定値 | 上限の理由 |
|---|
tomcat.version | 9.0.83 | 9.0.118 | 最新の 9.0.x; 10+ は jakarta.* |
spring-framework.version | 5.3.31 | 5.3.39 | Central 上の最後の OSS 5.3.x |
jackson-bom.version | 2.13.5 | 2.22.2 | 最新の 2.x |
log4j2.version | 2.17.2 | 2.26.1 | 最新の 2.x |
snakeyaml.version | 1.30 | 1.33 | 最後の 1.x; 残存 CVE の修正は 2.0 |
logback.version | 1.2.12 | 1.2.13 | 最後の 1.2.x — 下記参照 |
spring-security.version | 5.7.11 | そのまま | デモの主題である |
| 3C / 12H / 18M / 10L |
| メジャー内で修正できない理由 |
|---|
| spring-webmvc / -expression / -core / -context (25) | 5.3.39 が最後の OSS 5.3.x; webmvc の 15 件のうち 14 件には修正が一切なく、grype が挙げる 5.3.42 は商用専用 |
| logback-core (6) | SLF4J 2.x が必要、上記参照 |
| spring-security-* (8) | EOL 系; CVE-2026-22732 は意図的 |
| spring-boot / -autoconfigure (3) | 2.7.x 向けの修正は公開されていない |
| snakeyaml (1) | CVE-2022-1471 は 2.0 でのみ修正 |