
إثبات مفهوم يوضح 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 حرج — 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 للإشارة إلى مكان آخر، أو
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
فقط /vuln/content-length/account تتغيّر حالته عند ترقيع CVE، لذا فهو نقطة النهاية الوحيدة التي
يستمدّ منها 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 سمّت هذه "الاستغلال 2" وادّعت أنها الحالة التي توثّقها
النشرة. هي ليست جزءًا من CVE-2026-22732 ولا يصلحها أي ترقية.
CacheControlHeadersWriter متطابق بايت ببايت في 5.7.11 و5.7.14-0.cgr.2 و6.5.8 (آخر إصدار مصاب) و
6.5.9 (أول إصدار مُصلَّح) — تم التحقق بمقارنة ملفات المصادر jars. يذكر توثيقها Javadoc السلوك
صراحةً: "Inserts headers to prevent caching if no cache control headers have been
specified."
لا يزال يستحق العرض، لأن التسريب حقيقي والمخاطر المتبقية تنجو من الترقيع.
يتوقف CacheControlHeadersWriter إذا كان Cache-Control أو Expires أو Pragma موجودًا
بالفعل، لذا فإن ضبط أي واحد من الثلاثة يكبت كل توجيهات no-store الخاصة بـ Spring Security.
سطر واحد حسن النية يفعل ذلك:
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 كـ"موجود" في الجدول أعلاه، لكن بقيمة صديقة للمهاجم اختارها التطبيق —
تم استبدال Expires: 0 الخاص بـ Spring Security، لا مجرد إسقاطه. بيانات حامل البطاقة الآن قابلة
للتخزين من كل متصفح ووكيل مشترك على المسار حتى 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-security.version من 5.7.11 إلى 5.7.14-0.cgr.2 (و
spring-framework.version من 5.3.31 إلى 5.3.39-0.cgr.4) عبر
spring-boot-dependencies الخاص بالوالد. القياس:
| 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 |
بمقارنة ملفات المصادر jars الخاصة بـ spring-security-web، ملف واحد فقط يهم. 5.7.11 →
5.7.14-0.cgr.2 يضيف تجاوزات setHeader / setIntHeader / addIntHeader إلى
OnCommittedResponseWrapper، كل منها يوجّه Content-Length عبر setContentLength():