
وصفة OpenRewrite تكتشف وتصلح قمع ترويسات Spring Security (CVE-2026-22732) من خلال تحديد سوء استخدام ترويسة Content-Length وتوليد إعداد كتابة الترويسات المبكرة.
وصفة OpenRewrite تكتشف الكود المعرّض لـ CVE-2026-22732، وهو خلل في Spring Security حيث يؤدي تعيين Content-Length عبر إحدى طرق الاستجابة الثلاث إلى تجاوز OnCommittedResponseWrapper الخاص بـ Spring Security. نظرًا لأن الغلاف لا يرى الترويسة أبدًا، فإن onResponseCommitted() لا يتم استدعاؤه أبدًا، ويتم إسقاط ترويسات الأمان المضافة بتكاسل (X-Frame-Options, X-Content-Type-Options, Cache-Control, إلخ) بصمت.
المحفزات الفعلية، المؤكدة ضد Spring Security 6.4.12 الضعيف مع Spring Boot 3.4.3 / Tomcat المدمج:
Content-Length الخاص بـ Servlet عبر التحميلات الزائدة المتجاوزة للغلاف
response.setHeader("Content-Length", "42");
response.setIntHeader("Content-Length", 42);
response.addIntHeader("Content-Length", 42);
هذه التحميلات الزائدة الثلاثة غير مُعاد تعريفها في OnCommittedResponseWrapper. تكمل عمليات كتابة النص اللاحقة الطول المُعلن ويقوم الحاوية بالالتزام دون تشغيل كاتب الترويسات الكسول.
Content-Length الخاص بـ WebFlux عبر HttpHeaders
serverHttpResponse.getHeaders().set("Content-Length", "42");
serverHttpResponse.getHeaders().add("Content-Length", "42");
serverHttpResponse.getHeaders().setContentLength(42L);
التزامات استجابة WebFlux غير المشروطة
serverHttpResponse.writeWith(Mono.just(dataBuffer));
serverHttpResponse.writeAndFlushWith(publisher);
serverHttpResponse.setComplete();
الوصفة مشروطة بوجود Spring Security — لا تُصدر أي شيء في الملفات التي لا تشير إلى أي نوع org.springframework.security.* — وبإصدارات Spring Security المتأثرة. وفقًا لنشرة Spring الأمنية المنشورة في 2026-03-19، فإن النطاقات المتأثرة وإصدارات الإصلاح هي:
| السلسلة | المتأثر | المُصلح |
|---|---|---|
| 5.7.x | 5.7.0 – 5.7.21 | 5.7.22 (Enterprise) |
| 5.8.x | 5.8.0 – 5.8.23 | 5.8.24 (Enterprise) |
| 6.3.x | 6.3.0 – 6.3.14 | 6.3.15 (Enterprise) |
| 6.4.x | 6.4.0 – 6.4.14 | 6.4.15 (Enterprise) |
| 6.5.x | 6.5.0 – 6.5.8 | 6.5.9 (OSS) |
| 7.0.x | 7.0.0 – 7.0.3 | 7.0.4 (OSS) |
المشاريع التي تحل إصدار Spring Security عند أو فوق الإصلاح في سلسلتها (أو على أي سلسلة مستقبلية بعد 7.0 / 6.5) تُعتبر غير متأثرة ولا تتلقى أي علامات لكل نقطة تسريب أو لكل ملف. المشاريع التي لا يمكن حل إصدارها تنتقل إلى الكشف المعتاد القائم على الأنماط بحيث يميل الماسح إلى الإبلاغ عن نتيجة لا يمكنه دحضها. لا يزال جدول بيانات SpringSecurityVersionByProject يسجل الإصدار الذي تم حله ويضع علامة على كل مشروع كمتأثر أو غير متأثر، حتى تتمكن من تدقيق ما تمت تصفيته.
تبدو هذه خطيرة ولكن يتم تتبعها بواسطة الغلاف، لذلك تتم كتابة ترويسات الأمان قبل التزام الاستجابة:
| الكود | لماذا هو آمن |
|---|---|
response.setContentLength(int) / setContentLengthLong(long) | مُعاد تعريفه — يسجل الغلاف الطول المُعلن ويشغل onResponseCommitted() عند اكتمال النص. |
response.flushBuffer() | مُعاد تعريفه — يستدعي doOnResponseCommitted() قبل super.flushBuffer(). |
response.getOutputStream().write(..) / flush() / close() | يُرجع SaveContextServletOutputStream؛ كل كتابة/تفريغ/إغلاق يشغل doOnResponseCommitted() قبل التفويض. |
response.getWriter().write(..) / print(..) / println(..) / flush() / close() | يُرجع SaveContextPrintWriter؛ نفس النمط. |
response.addHeader("Content-Length", v) | حالة خاصة في الغلاف — يتم توجيهها عبر setContentLength(long). |
يدّعي نقطة نهاية /vuln/flush في عرض Semgrep أن flushBuffer() هو المحفز، ولكن على Spring Security 6.4.12 الضعيف، تُرجع الاستجابة فعليًا جميع ترويسات الأمان الستة. المحفزات الحقيقية في العرض هي استدعاءات setIntHeader("Content-Length", ...) في /vuln/stream و /vuln/content-length.
شغّل هذه الوصفة:
| الوصفة | الغرض |
|---|---|
io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression | تشغيل كل اكتشاف وإصدار جدول تقرير الإصدار |
المُجمِّع أعلاه يتكون من وصفتين أصغر. يمكنك استدعاؤهما بشكل فردي إذا كنت تريد اكتشافًا واحدًا فقط.
| الوصفة | الغرض |
|---|---|
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthHeader | تدفق التلوث للحرف "Content-Length" الذي يصل إلى setHeader / setIntHeader / addIntHeader (servlet) أو HttpHeaders.set / add (WebFlux) |
io.moderne.recipe.cve202622732.FindHttpResponseContentLengthOrFlushBuffer | التزامات WebFlux غير المشروطة: writeWith, writeAndFlushWith, setComplete, HttpHeaders.setContentLength |
شغّل هذه الوصفة:
| الوصفة | الغرض |
|---|---|
io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression | اختيار أرخص معالجة يمكن لكل مشروع تنفيذها فعليًا |
تشغّل خطوتين بالترتيب.
1. الترقية إلى الإصلاح على سلسلة المشروع الخاصة. نشر Spring Security الإصلاح كإصدار 6.5.9
و 7.0.4 على Maven Central. كل ترقية مشروطة بشرط مسبق FindAffectedSpringSecuritySeries،
لأن UpgradeDependencyVersion يتحقق فقط من أن هدفه أحدث — إذا طُلب منه
الانتقال إلى 7.0.4 فسيسحب مشروع 5.8 عبر إصدارين رئيسيين عن طيب خاطر.
2. إضافة تكوين كتابة ترويسات مبكر لأي شيء لم تستطع الخطوة 1 إصلاحه. يولّد هذا
فئة @Configuration واحدة لكل مشروع:
@Bean
public static BeanPostProcessor eagerHeaderWriterFilterBeanPostProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof HeaderWriterFilter) {
((HeaderWriterFilter) bean).setShouldWriteHeadersEagerly(true);
}
return bean;
}
};
}
كتابة الترويسات مسبقًا تجعل من غير المهم ما إذا كان الغلاف يلاحظ الالتزام أم لا، لذا
يغلق هذا كل نقطة تسريب في المشروع دفعة واحدة — بما في ذلك تلك التي لا يمكن لتحليل التلوث الوصول إليها،
مثل تدفق Map تحت "القيود المعروفة". يرى BeanPostProcessor عوامل التصفية المبنية بواسطة
DSL HttpSecurity لأن AutowireBeanFactoryObjectPostProcessor يهيئها عبر
مصنع الفول. إصلاحان منشوران بشكل مستقل لهذا CVE يستخدمان هذا الشكل بالضبط
(hmcts/idam-web-public، و
فروع armory-io الخاصة بـ Spinnaker عبر ObjectPostProcessor المكافئ).
الخطوة 2 هي ما يغطي المشاريع التي لا يمكن للخطوة 1 مساعدتها:
| الموقف | لماذا لا تعمل الترقية |
|---|---|
| 5.7, 5.8, 6.3, 6.4 | الإصلاح يُشحن لمشتركي Spring Enterprise فقط — وليس على Maven Central |
| 6.0 - 6.2 | لم يُصدر أي إصلاح على تلك السلاسل |
| الإصدار المُدار بواسطة BOM مستورد | لا يوجد شيء مُعلن محليًا لتقوم الترقية بتعديله |
الصف الأخير ليس حالة هامشية. nla/bamboo يحل 7.0.3 — سلسلة لديها إصلاح
مفتوح المصدر — بالكامل من Spring Boot BOM، لذا فإن فحص الإصدار وحده سيتخطاه تحت كلتا الخطوتين
ويتركه عرضة للخطر. لذلك AddEagerHeaderWriterConfiguration يؤجل فقط إلى الترقية
عندما يكون لدى المشروع إصلاح مفتوح المصدر متاح و يعلن إصدارًا خاصًا به.
تُوضع الفئة المُولَّدة بجوار فئة @EnableWebSecurity حيثما توجد، مع الرجوع
إلى @SpringBootApplication ثم أي @Configuration، لذا تهبط دائمًا في مكان يصل إليه
فحص المكونات. المشاريع التي تكتب الترويسات مبكرًا بالفعل، أو التي تورد
OnCommittedResponseWrapper مُصححًا (كما يفعل jogetworkflow/jw-community)،
تُترك دون تغيير.
| الوصفة | الغرض |
|---|---|
io.moderne.recipe.cve202622732.UpgradeSpringSecurityToPatchedVersion | ترقية مشروطة بالسلسلة إلى 6.5.9 / 7.0.4 |
io.moderne.recipe.cve202622732.AddEagerHeaderWriterConfiguration | التكوين المُولَّد، بمفرده |
io.moderne.recipe.cve202622732.FindAffectedSpringSecuritySeries | وضع علامة على المشاريع على سلسلة متأثرة واحدة؛ الشرط المسبق للترقية |
طُبِّق على semgrep/cve-2026-22732-demo على
Spring Security 6.4.12 الضعيف، ضد Tomcat المدمج. يؤكد HeaderVerificationTest الخاص به
أن ترويسات الأمان مفقودة، لذا فإن الإصلاح الناجح يجعله يفشل:
| نقطة النهاية | قبل | بعد |
|---|---|---|
/vuln/stream | X-Content-Type-Options: null | nosniff |
/vuln/content-length | X-Content-Type-Options: null | nosniff |
/safe | nosniff | nosniff |
X-Frame-Options و Cache-Control يتبعان نفس النمط.
عبر المستودعات الـ 16 من المجموعة التي تُبنى (من أصل 29 تم تحديدها)، ولّد الإصلاح تكوينًا لثلاثة وترك الباقي دون تغيير بشكل صحيح:
| المستودع | النتيجة |
|---|---|
semgrep/cve-2026-22732-demo | مُولَّد في com/example/vuln، بجوار @EnableWebSecurity؛ يُترجم، الترويسات مستعادة |
nla/bamboo | مُولَّد في ui/src/bamboo، بجوار @SpringBootApplication؛ يُترجم. حالة 7.0.3 المُدارة بواسطة BOM التي لا يمكن للترقية الوصول إليها |
star-whale/starwhale | مُولَّد في ai/starwhale/mlops/configuration/security، بجوار @EnableWebSecurity؛ يُترجم (JDK 11، هدفه المُعلن) |
hmcts/idam-web-public | تُرك دون تغيير — يستدعي بالفعل setShouldWriteHeadersEagerly |
okta/okta-idx-java (6.5.9), psi-probe (6.5.11), Ant-Media-Server (6.5.11) | تُرك دون تغيير — بعد الإصلاح على سلسلتها |
apache/shenyu (6.3.1) | تُرك دون تغيير — تفاعلي فقط؛ HeaderWriterFilter لا يحتوي على servlet API خلفه، و CVE خاص بـ servlet فقط |
brutusin/Brutusin-RPC (4.0.4) | تُرك دون تغيير — يسبق setShouldWriteHeadersEagerly (5.2) |
bootplus, template-app, front50, igor, rosco, spring-security, reportserver | تُرك دون تغيير — لم يتم حل أي إصدار Spring Security متأثر |
إعادة تشغيل الإصلاح على المستودعات الثلاثة المُصححة لا يولّد أي شيء إضافي، لذا فإن المعالجة مُتسقة ذاتيًا ضد مخرجاتها على المشاريع الحقيقية.
مجموعة ثانية أوسع تستهدف السكان الذين يعالجهم الإصلاح فعليًا — أي تطبيق servlet Spring Security متأثر،
نظرًا لعدم الحاجة إلى أي نقطة تسريب. من بين 64 مشروعًا من هذا القبيل تم العثور عليها بواسطة بحث الكود، بُني 52، وحل 48 إصدارًا متأثرًا، و تم تصحيح 38؛ 35 منها تُترجم (الثلاثة الآخرون يفشلون بشكل مماثل بدون الملف المُولَّد). تم تخطي جميع المشاريع الثمانية على Spring Security أقل من 5.2 بشكل صحيح. راجع SUSCEPTIBLE-REPOSITORIES.md القسم 8.
OnCommittedResponseWrapper extends jakarta.servlet.http.HttpServletResponseWrapper، لذا
فإن CVE-2026-22732 خاص بـ servlet فقط والتطبيق التفاعلي غير معرّض له. النتائج من
FindHttpResponseContentLengthOrFlushBuffer تضع علامة على النمط التفاعلي المماثل وما زالت تحتاج
إلى مراجعة يدوية، لكن الإصلاح لا يتصرف بناءً عليها عمدًا. AddEagerHeaderWriterConfiguration
يتخطى أي وحدة يمكنها رؤية HeaderWriterFilter بدون servlet API خلفها —
عامل التصفية يمتد OncePerRequestFilter، و spring-security-web يحمل servlet API كاعتماد
provided غير انتقالي، لذا فإن التوليد هناك يفشل مع
cannot access jakarta.servlet.Filter (لوحظ على apache/shenyu).HeaderWriterFilter.setShouldWriteHeadersEagerly يصل في 5.2؛ على 4.0.4 يحتوي عامل التصفية على
مُنشئ و doFilterInternal فقط. لا يزال الاكتشاف يُبلغ عن إصدارات نهاية العمر، لكن المعالجة
تُحجب بدلاً من إصدار استدعاء لا يمكن ترجمته.| الجدول | الصفوف |
|---|---|
TaintFlowTable (من rewrite-program-analysis) | صف واحد لكل إصابة تلوث ترويسة Content-Length |
HttpResponseDirectCommitTable | صف واحد لكل إصابة هيكلية WebFlux |
SpringSecurityVersionByProject | صف واحد لكل مشروع مع إصدار Spring Security مكتشف |
عبر Moderne CLI:
mod run . --recipe io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
عبر rewrite.yml:
---
type: specs.openrewrite.org/v1beta/recipe
name: com.example.DetectSpringSecurityHeaderSuppression
displayName: Detect CVE-2026-22732
recipeList:
- io.moderne.recipe.cve202622732.FindSpringSecurityHeaderSuppression
يسرد repos.csv المستودعات العامة الـ 75 التي تم تطوير وقياس هذه الوصفات
ضدها، مثبتة على الالتزام المحدد الذي تم تقييم كل منها عنده. العديد منها مُدار بنشاط
وسيتم تصحيحه في المصدر، لذا فإن عمود changeset هو ما يجعل الأرقام أدناه
قابلة لإعادة الإنتاج بدلاً من كونها معقولة فقط.
mod git sync csv ./corpus repos.csv --with-sources
mod build ./corpus
mod run ./corpus --recipe=io.moderne.recipe.cve202622732.CveDevCenter
mod devcenter ./corpus --last-recipe-run
تستغرق المزامنة حوالي 20 ثانية و 1.4 جيجابايت؛ يستغرق البناء حوالي 15 دقيقة وهو
الخطوة البطيئة الوحيدة. يكتب mod devcenter ملف devcenter.html في
corpus/.moderne/run/<runId>/، بالإضافة إلى واحد لكل دليل فرعي للمؤسسة.
يقسم عمود org1 المجموعة إلى مجموعات تعكس سبب وجود كل مستودع
— Servlet Sinks و WebFlux Sinks لشكلي الاستدعاء الضعيفين، Patched لـ
المستودعات المعالجة بالفعل في المصدر، Reference لـ Spring Security نفسه والمستهلكين
الآخرين، Verified للحالة المفحوصة ضد خادم قيد التشغيل، Gradle لـ
تغطية أدوات البناء، و Wide للعينة الأكبر.
توقع، على الالتزامات المثبتة:
| النتيجة | |
|---|---|
| بطاقة الترقية | 39 Major، 21 Minor، 6 Patch، 4 Completed (70 مستودعًا) |
| بطاقة الأمان | 65 مستودعًا مكشوفًا |
| غير قابل للتطبيق | 5 مستودعات لا تحل أي اعتماد Spring Security |
أربعة من تلك الخمسة لا تستخدم Spring Security حقًا — spring-projects/spring-security
هي المكتبة نفسها، JoeyBling/bootplus يستخدم Apache Shiro، jenkinsci/stapler يستهدف
servlet API مباشرة، و infofabrik/reportserver لا يحتوي على بناء Maven أو Gradle
لحله. الخامس، xtuer/template-app، يعلن spring-security-web:5.0.0.RELEASE ولكن
بناء Gradle الخاص به لا يحل أي اعتماديات على الإطلاق أثناء mod build، لذا لا يمكن لأي وصفة رؤية
الإصدار. عالجه كغير مُقاس بدلاً من غير متأثر.
لتطبيق الإصلاح والتحقق من النتيجة:
mod run ./corpus --recipe=io.moderne.recipe.cve202622732.FixSpringSecurityHeaderSuppression
mod git apply ./corpus --last-recipe-run
mod exec ./corpus --last-recipe-run MODERNE_BUILD_TOOL_CHECK
يكتب mod git apply إلى النسخ المحلية في مكانها. فحص check كامل عبر المجموعة بطيء
وسيُظهر إخفاقات غير مرتبطة بهذا التغيير — سلاسل أدوات JDK مفقودة، مستودعات
اعتماديات غير قابلة للوصول، اختبارات كانت حمراء بالفعل — لذا فإن الإشارة المهمة هي
الفرق مقابل نفس الأمر الذي تم تشغيله قبل التطبيق.
يتعامل تحليل التلوث من rewrite-program-analysis مع تدفق البيانات المحلي وملخصات لكل طريقة، لذا يتم اكتشاف هذه الأنماط تلقائيًا:
String h = "Content-Length"; response.setIntHeader(h, 42); يتم وضع علامة عليه — يتتبع الإطار التلوث الحرفي عبر التعيين المحلي.Map, List, مجموعات مخصصة). stash.put("k", "Content-Length") متبوعًا بـ response.setIntHeader(stash.get("k"), 42) لا يتم اكتشافه — هوية put/get غير شفافة للتحليل.Moderne Proprietary. للاستخدام فقط من قبل عملاء Moderne بموجب شروط عقد تجاري.