
مختبر كشف استمرارية الإبطال: عندما تنجح إعادة تعيين كلمة المرور لكن المهاجم لا يغادر أبدًا. يعيد إنتاج ثغرة الإبطال الشرطي Strapi CVE-2026-22706، وإصلاحها، وحزمة كشف من ثلاث قواعد، والقاعدة الساذجة التي تفوتها.
عندما تنجح إعادة تعيين كلمة المرور لكن المهاجم لا يرحل أبدًا.
مختبر محلي أحمر/أزرق لفئة ثغرة واحدة: بيانات الاعتماد التي تعيش أطول من الحدث الذي كان من المفترض أن يقتلها. يوفّر الاستغلال، والسبب الجذري، والإصلاح، وحزمة كشف من ثلاث قواعد، وتدقيق حالة لما لا تستطيع القواعد رؤيته بنيويًا، ووحدة تحكم جنائية — وقاعدة الكشف التي لا تعمل، محفوظة في المستودع ليُبرهن على فشلها.
بدء سريع · الاكتشاف · لماذا يفشل الكشف الساذج · حزمة القواعد · وحدة التحكم · الإصلاح · المصفوفة · الاختبارات · التوثيق
الهجوم نفسه. الطلبات نفسها. فرق واحد.
مُولَّد بواسطة scripts/figures.py من الحمولة نفسها التي
ترسمها وحدة التحكم — يُعاد توليده ومقارنته في CI، حتى لا ينحرف الشكل عن الكود.
09:00 alice logs in ┐ refresh credential rt-001 │ the attacker steals rt-001 ┘
10:00 alice changes her password ← the one thing a victim can do alone HTTP 200 · password changed · fresh session issued
10:00:03 attacker: POST /refresh rt-002 → HTTP 200 new access credential at-004, issued 10:00:03
10:01:00 attacker: GET /me at-004 → HTTP 200 {"username": "alice", "authenticated": true}
لا خطأ. لا شذوذ. لا فشل في المصادقة ليُحتسب. بيانات اعتماد وصول المهاجم عمرها *ثلاث ثوانٍ* وقد سكّها الخادم، عند الطلب، بعد إعادة التعيين.
إليك الثغرة الأمنية بأكملها:```python
def _revoke_for_security_change(self, user, kind, device_id):
if device_id:
revoke_credentials(user, device_id=device_id) # ← the finding
اقرأ ذلك بالطريقة التي سيقرؤها بها المراجع. منطق الإبطال موجود هناك تمامًا. يستدعي
الدالة الصحيحة بالنطاق الصحيح. نقطة النهاية المحيطة به تحدّث
تجزئة كلمة المرور، وتعيد 200، وتصدر جلسة جديدة — كل سلوك ملحوظ
لتغيير كلمة مرور صحيح موجود.
وإذا حذف المستدعي device_id، لا يتم إبطال أي شيء، ولا تزال
نقطة النهاية تبلّغ عن النجاح.
هذا ليس افتراضيًا. إنه
CVE-2026-22706
في Strapi ≤ 5.33.2، حيث كانت خطوة إبطال رمز التحديث مشروطة
بـ deviceId المقدَّم من المستدعي. حُصل على تقييم 2.1، منخفض. نوقش
أدناه.
التقييم منخفض لأن المهاجم كان لديه وصول بالفعل — هذا هو شرط الدخول، وهذه الثغرة لا تمنح شيئًا جديدًا. ما تمنحه هو المدة، وهي تفعل ذلك عبر كسر الضابط الوحيد الذي يمكن للضحية تشغيله بنفسه.
test_persistence_lasts_as_long_as_the_refresh_credential يمشي سبعة أيام من
الوقت المحاكى لإظهار ذلك.ثغرة بتقييم منخفض في مسار احتواء تكلّف أكثر من ثغرة بتقييم منخفض في مسار ميزة، لأن التكلفة تُدفع أثناء حادثة، حين لا يقرأ أحد التنبيهات.
القاعدة التي تكتبها أولًا:```text IF credential.issued_at < credential_change.timestamp: ALERT
إنها ليست قاعدة غبية. فهي رخيصة، وتحتاج إلى حقل واحد، وتُقرأ كما لو كانت تعريفًا
للمشكلة، وهي **صحيحة بشأن الحالة البسيطة** — فالمهاجم الذي يستخدم
رمز *وصول* مسروقًا بعد إعادة التعيين يتم اكتشافه.
يؤكد `test_the_naive_rule_catches_the_simple_case` أنها تعمل.
كلا القاعدتين تطرحان السؤال نفسه عن الاعتماد نفسه. لكنهما تقرآن حقلين
مختلفين للإجابة عنه، والحقلان يتناقضان:

**بعد ثلاث ثوانٍ، أم قبل ساعة — الاعتماد نفسه، في اللحظة نفسها.** القاعدة الساذجة تسأل عن حقل يتحكم فيه المهاجم، وطلب `POST /refresh` واحد يعيد ضبطه.
شغّلها على سجل الطلبات، وهو المكان الذي تُكتب فيه هذا الاستعلام فعليًا،
لأن سجلات الطلبات هي ما لديك:```text
NAIVE DETECTOR (NAIVE-001), over the request log
Result: NO ALERT
Six requests were served to the attacker after the reset. Every one of them
carried at-004, minted at 10:00:03 -- three seconds *after* the password
change. By its own timestamp it is the newest credential on the account.
سيكون من السهل التوقف عند هذا الحد، ومن غير الأمانة، لذا يُشغّل المختبر أيضًا النسخة الأكثر إنصافًا من القاعدة الساذجة — موسّعة لمراقبة نقطة نهاية التحديث أيضًا:```text NAIVE DETECTOR, widened to include POST /refresh
1 alert at 2026-09-11T10:00:03.000Z: the credential presented to /refresh was rt-002, issued 2026-09-11T09:30:00.000Z. Then it goes blind. 6 events follow that hop and it flags none of them, because every credential from there on carries a post-reset timestamp. Its incident covers 1 accepted request; the lineage rule's covers all of them.
And look at what the alert names: NAIVE-001 revoke rt-002 -- rotated away and already dead at 10:00:03 AFTERLIFE-001 revoke lin-001 -- the live thing every future credential descends from
هذا هو الفرق الذي يهم في الثالثة صباحًا. تلتقط القاعدة الساذجة اللحظة الواحدة التي تعبر فيها السلسلة الحدود، ثم تفقد الأثر خلال الـ 29 يومًا المتبقية — والبيانات الاعتمادية التي تسميها *قد تم تدويرها بالفعل وإبطالها من قبل الخادم*. إبطالها لا يحقق شيئًا. `lin-001` هو الكائن الذي يجب عليك قتله.
**ولم تكن القاعدة الساذجة محرومة من القياس عن بُعد.** فهي تستخدم نفس تدفق الأحداث، ونفس فهرس النسب، ونفس التسامح، ونفس إزالة التكرار، ونفس الحالة المحدودة. إنها تتجاوز طريقة واحدة:```python
class Correlator: # AFTERLIFE-001
def _age_reference(self, facts):
return facts.root_issued_at
class NaiveCorrelator(Correlator): # NAIVE-001
def _age_reference(self, facts):
return facts.issued_at
root_issued_at موجود بالفعل في الفهرس الذي يستشيره.
test_the_naive_rule_had_the_data_it_needed يثبت ذلك. الفشل في
المقارنة، وليس في التسجيل.
ثلاث قواعد على سجل واحد. تجيب على أسئلة مختلفة، وتُطلق بالنorder الذي تتطور به الحادثة فعلياً.