
CVE-2026-22732 का प्रमाण-अवधारणा प्रदर्शन, जो Spring Security की एक खामी है जहाँ setIntHeader("Content-Length") सभी सुरक्षा हेडर हटा देता है, जिसमें असुरक्षित और पैच किए गए बिल्ड शामिल हैं।
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, एक <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 उस JDK 25 पर नहीं चल सकता जिसे इस मशीन पर
mvn डिफ़ॉल्ट रूप से रिज़ॉल्व करता है। JAVA_HOME_17=/path/to/jdk17 से ओवरराइड करें।
यह डिफ़ॉल्ट रूप से -s settings-chainguard.xml के साथ भी बिल्ड करता है, क्योंकि पैच किया गया पैरेंट
Maven Central पर नहीं है। कहीं और इंगित करने के लिए MAVEN_SETTINGS=/path/to/your/settings.xml सेट करें, या
पूरी तरह Central से बिल्ड करने के लिए MAVEN_SETTINGS= — जो केवल स्टॉक 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) → पूर्ण बायपाससाधारण दिखने वाले कंट्रोलर कोड की तीन पंक्तियाँ 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 नहीं। /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 (पहला ठीक किया गया) में बाइट-समान है — सोर्स जार को डिफ करके सत्यापित। इसका Javadoc व्यवहार को
स्पष्ट रूप से बताता है: "Inserts headers to prevent caching if no cache control headers have been
specified."
यह अभी भी प्रदर्शित करने योग्य है, क्योंकि लीक वास्तविक है और अवशिष्ट जोखिम पैचिंग के बाद भी बना रहता है।
CacheControlHeadersWriter बाहर निकल जाता है यदि Cache-Control, Expires या Pragma पहले से
मौजूद हो, इसलिए तीनों में से किसी एक को सेट करना Spring Security के सभी no-store
निर्देशों को दबा देता है। एक अच्छे इरादे वाली पंक्ति यह कर देती है:
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 पर यह
गलत है। दोनों एंडपॉइंट सभी छह हेडर देते हैं।
/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 एडवाइज़री स्वयं कभी कमिट-ऑर्डरिंग कहानी का समर्थन नहीं करती।
इन दो एंडपॉइंट को बनाए रखना PoC को मिथ्यापनीय बनाता है: यह दिखाता है कि क्या नहीं पुनरुत्पादित होता है उतनी ही स्पष्टता से जितना कि क्या होता है, और दोनों पैच के दौरान हरे रहते हैं, जो उस एक एंडपॉइंट को सार्थक बनाता है जो वास्तव में बदलता है।
pom.xml में एक पंक्ति, और कुछ नहीं। कोई सोर्स परिवर्तन नहीं, कोई प्रॉपर्टी परिवर्तन नहीं, कोई 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 पर) फिर से पिन करता है। मापा गया: