
إثبات مفهوم يوضح 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 الخاص بالوالد. القياس:
بمقارنة ملفات المصادر jars الخاصة بـ spring-security-web، ملف واحد فقط يهم. 5.7.11 →
5.7.14-0.cgr.2 يضيف تجاوزات setHeader / setIntHeader / addIntHeader إلى
OnCommittedResponseWrapper، كل منها يوجّه 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 (أول إصدار
مُصلَّح)، لذا هذا هو الإصلاح الرسمي منقولًا عكسيًا، وليس إعادة تنفيذ. يضيف بناء Chainguard
حارسين ضد القيم الفارغة لا يملكهما 6.5.9 الرسمي (value != null على تحميل String، و
(csq != null) ? csq.length() : 4 في append).
Spring Security 5.7.x وصل إلى نهاية العمر الرسمي؛ إصلاحات OSS تصل فقط في 6.4.15 / 6.5.9 / 7.0.4+. إذا لم يكن إصدار 5.7.x المُعاد بناؤه خيارًا:
HeaderWriterFilter.shouldWriteHeadersEagerly = true عبر
ObjectPostProcessor. وفقًا للنشرة، هذا يغيّر السلوك: الرؤوس المكتوبة من التطبيق عندئذ
تتجاوز رؤوسًا محددة فقط بدلًا من كبت رؤوس Spring Security. هذا أيضًا يصلح
/vuln/cache/account، وهو ما لا تفعله ترقية الإصدار.لا شيء من هذه مُدمج في هذا المشروع، لذا السلوك المصاب هو الافتراضي والحالة
المُرقَّعة يمكن الوصول إليها بتغيير <version> الواحد أعلاه.
يثبّت Boot 2.7.18 إصدار Tomcat 9.0.83، الذي يضع عليه grype . علامة 34 ثغرة CVE (4 حرجة). كلها
مُصلَّحة في 9.0.118 أو أقل، و9.0.118 هو أحدث إصدار 9.0.x — لذا خاصية واحدة تُزيل المجموعة:
<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 على أنواع servlet.
نفس الآلية مطبَّقة على بقية التبعيات المُدارة في Boot 2.7.18. لا ترقيات إصدار رئيسي، ولا ترقية Spring Boot:
تتفاعل هذه التجاوزات مع الوالد المُرقَّع، لذا اعرف ما تفعله قبل العرض. على
2.7.18-0.cgr.3 يوفّر الوالد بالفعل tomcat.version 9.0.118 وlogback.version 1.2.13 و
snakeyaml.version 1.33 — تصبح هذه الصفوف الثلاثة مكرّرة تمامًا ويمكن حذفها دون
تغيير أي شيء. صفّا jackson-bom.version وlog4j2.version لا يزالان يقومان بعمل حقيقي:
يحتفظ الوالد المُرقَّع بإصدارات Boot الأصلية 2.13.5 / 2.17.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، الحرجة 39 → 12.
تم التحقق بعد كل ترقية: يُقلع التطبيق على Tomcat/9.0.118، وتُعيد نقاط النهاية الستة جميعًا 200، و
تُعاد الثغرة بايت ببايت. لا يزال Spring Boot 2.7.18 وSpring Security لا يزال 5.7.11، لذا
CVE-2026-22732 دون مساس — وهذا هو مغزى هذا القسم، وكذلك حدّه الصادق:
ترقيع كل شيء حوله لا يفعل شيئًا لثغرة إطار التطبيق. إصلاح تلك يحتاج
ترقية الوالد، لا خاصية.
أحدث 1.x هو 1.6.3 — نفس الإصدار الرئيسي، لذا اسميًا داخل النطاق. لا يعمل. استبدل Logback 1.3+
StaticLoggerBinder من SLF4J 1.7 بمزوّد ServiceLoader من SLF4J 2.x، ويستدعي
LogbackLoggingSystem في Boot 2.7 الدالة 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 (2 متوسطة، 4 منخفضة) غير قابلة للإصلاح على هذا الخط.
اثنتان من الثلاث ثغرات الحرجة المتبقية تستحقان القراءة بعناية بدلًا من الاعتماد على الدرجة:
HttpInvokerServiceExporter. لا يستخدم هذا التطبيق HTTP Invoker، لذا فهو غير قابل للوصول هنا.5.7.14-0.cgr.2 تجاوز إصدار الإصلاح.البقايا بنيوية: Spring Framework 5.3.x وSpring Security 5.7.x كلاهما منتهي العمر. هذا، وليس 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 دون مصادقة — لا هذا ولا ذاك جزء من هذه
الثغرة.
| 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 | آخر 5.3.x من OSS على Central |
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 | تُرك كما هو | هو موضوع العرض |
| + كل ترقيات داخل الخط الرئيسي |
| 43 |
| 3C / 12H / 18M / 10L |
| المكوّن | لماذا لا يمكن إصلاحه داخل الخط الرئيسي |
|---|
| spring-webmvc / -expression / -core / -context (25) | 5.3.39 هو آخر 5.3.x من OSS؛ 14 من 15 نتيجة webmvc ليس لها إصلاح على الإطلاق، و5.3.42 الذي يذكره grype تجاري فقط |
| logback-core (6) | يحتاج SLF4J 2.x، انظر أعلاه |
| spring-security-* (8) | خط منتهي العمر؛ CVE-2026-22732 متعمّدة |
| spring-boot / -autoconfigure (3) | لا إصلاح منشور لـ 2.7.x |
| snakeyaml (1) | CVE-2022-1471 مُصلَّحة فقط في 2.0 |