في دليل حماية تطبيقات الويب بطبقات أمان في ال Reverse Proxy وضعنا جدار حماية تطبيقات الويب Web Application Firewall واختصاراً WAF أمام تطبيق تجريبي، وشرحنا فكرة النقاط Anomaly Scoring ومستوى التشدد Paranoia Level، وكيف ترفض القواعد نصاً تقنياً مشروعاً في صفحة ويكي. وبعد ذلك يأتي السؤال العملي: أي WAF تضعه على سيرفرك؟ والخيارات المشهورة المفتوحة أو المجانية أربعة:
- محرك ModSecurity مع قواعد OWASP CRS، وهو الأقدم والأكثر انتشاراً.
- محرك Coraza المكتوب بلغة Go، وينفذ نفس القواعد.
- أداة BunkerWeb التي تجمع ModSecurity وCRS وطبقات أخرى في Reverse Proxy جاهز.
- أداة SafeLine من شركة Chaitin، التي تجاوزت عشرين ألف نجمة على GitHub وتعد بإنذارات خاطئة أقل بكثير.
ولن نجيب عن السؤال من صفحات المشاريع، وإنما من تجربة: نفس التطبيق خلف كل جدار، ونفس قائمة الهجمات من مصدر عام، ونفس 45 رسالة عربية حقيقية الشكل يكتبها زبائن متجر عادي. وقد يتساءل البعض: لماذا الرسائل العربية بالذات؟ والإجابة أن هذه القواعد كتبت وجربت على نصوص إنجليزية في الغالب، وسوف ترى بعد قليل أن ثلاث رسائل عربية عادية جداً، إحداها اسم «أسماء»، رفضتها كل الجدران في تجربتنا بنفس القاعدة، والسبب في طريقة تحويل الحروف العربية قبل الفحص.
وسوف نناقش في هذا المقال ما يلي:
- الخيارات الأربعة: ما هو كل واحد منها، ومن يطوره، وبأي رخصة، وما المفتوح فيه فعلاً.
- تجربة عملية على ModSecurity وBunkerWeb وCoraza: نسبة اكتشاف الهجمات، والإنذارات الخاطئة على النص العربي، والتأخير الذي يضيفه كل جدار.
- لماذا يرى المحرك اسم «أسماء» كأنه حقن SQL، وكيف تكتب استثناء Rule Exclusion ضيقاً يصلح ذلك دون أن تفتح ثغرة.
- الإعدادات الافتراضية في BunkerWeb التي قد تحجب زبائنك قبل المهاجمين.
- ما وجدناه عندما ثبتنا SafeLine، ولماذا لم نكمل التجربة عليه.
- جدول قرار: أي جدار يناسب أي حالة.
كيف يقرر الجدار أن الطلب هجوم؟
الجدار يقف بين الزائر والتطبيق، فيقرأ كل طلب قبل أن يصل إلى التطبيق، ويمرره على مئات القواعد Rules، وكل قاعدة تطابق تضيف نقاطاً إلى الطلب، فإذا تجاوز المجموع الحد المحدد رفض الطلب بالرمز 403. وقبل أن تطبق القاعدة على النص يمر النص على سلسلة تحويلات Transformations، مثل فك ترميز الرابط URL Decoding وتحويل الحروف الكبيرة إلى صغيرة، والسبب أن المهاجم يحاول إخفاء الهجوم بالترميز، فالتحويلات تعيد كل الأشكال إلى شكل واحد قبل المقارنة. وهذه التحويلات نفسها هي مصدر المشكلة مع العربية كما سيأتي.
الصورة التالية تبين مسار الطلب داخل الجدار:

وتفاصيل النقاط ومستويات التشدد الأربعة شرحناها في دليل الطبقات، فلن نعيدها هنا، ويكفي أن تتذكر أن المستوى 1 هو الافتراضي وأقل المستويات إنذارات خاطئة، وأن كل مستوى أعلى يضيف قواعد أكثر حساسية، فيكتشف أكثر ويخطئ أكثر.
الخيارات الأربعة
لاحظ أن ثلاثة من الأربعة تشترك في مجموعة القواعد نفسها، وهي OWASP Core Rule Set واختصاراً CRS برخصة Apache-2.0، أما الاختلاف فهو في المحرك الذي ينفذها وفي ما يضاف حوله. والجدول التالي يلخصها كما في أكتوبر 2026:
| الأداة | المحرك والقواعد | الرخصة | آخر إصدار راجعناه |
|---|---|---|---|
| ModSecurity | محرك مكتوب بلغة C++ مع قواعد CRS | Apache-2.0 | الإصدار 3.0.17 في 29 سبتمبر 2026 |
| Coraza | محرك مكتوب بلغة Go يقرأ قواعد CRS نفسها | Apache-2.0 | الإصدار 3.8.1 في 2 أكتوبر 2026 |
| BunkerWeb | خادم Nginx مع ModSecurity وCRS وطبقات أخرى | رخصة AGPL-3.0 مع نسخة PRO مدفوعة | الإصدار 1.6.15 في 21 سبتمبر 2026 |
| SafeLine | محرك تحليل دلالي Semantic Analysis خاص بالشركة | المستودع GPL-3.0 والمنتج باتفاقية ترخيص خاصة | الإصدار 9.4.2 في 28 سبتمبر 2026 |
محرك ModSecurity مع قواعد CRS
هو المحرك الأقدم والأكثر انتشاراً، وقد ذكرنا في دليل الطبقات أن رعايته انتقلت من Trustwave إلى OWASP، وما زال المشروع يصدر تحديثات منتظمة، فآخرها الإصدار 3.0.17 من الفرع الثالث مع إصدار 2.9.15 من الفرع القديم في نفس اليوم. أما القواعد فمشروع مستقل هو CRS، وآخر إصداراته 4.30.0 في 2 أكتوبر 2026، مع فرع دعم طويل LTS هو 4.25. وأسهل طريقة لتشغيلهما معاً هي ال Image الرسمي owasp/modsecurity-crs الذي استخدمناه في دليل الطبقات.
- نقاط القوة: أكثر محرك مختبر في العالم، ووثائق CRS واسعة، وكل أداة أخرى في السوق تقريباً تشرح استثناءاتها بلغته.
- نقاط الضعف: لا توجد واجهة إدارة، فكل شيء ملفات إعداد وسجلات JSON، وضبط الاستثناءات يحتاج من يفهم القواعد.
محرك Coraza
هو محرك مكتوب بلغة Go ينفذ لغة قواعد ModSecurity نفسها، ويشغل CRS دون تعديل، ويعمل كمكتبة داخل برامج Go، ولذلك تجده إضافة في Caddy عبر coraza-caddy، وفي Envoy عبر WebAssembly. وفي تجربتنا بنينا Caddy 2.11.7 مع coraza-caddy 2.6.2، وفيها Coraza 3.8.1 وقواعد CRS 4.25.0 مدمجة.
- نقاط القوة: إذا كنت تستخدم Caddy فهو جزء من ال Reverse Proxy نفسه دون Container إضافي، وهو من مشاريع OWASP المصنفة Production Project برخصة Apache-2.0.
- نقاط الضعف: تحتاج إلى بناء Caddy بنفسك بالأداة xcaddy عند كل تحديث، والقواعد المدمجة تتأخر قليلاً عن آخر إصدار من CRS، ففي تجربتنا كانت 4.25.0 بينما آخر إصدار 4.30.0.
أداة BunkerWeb
هي Reverse Proxy مبني على Nginx من شركة Bunkerity، وفيه ModSecurity وCRS مفعلان افتراضياً، ومعهما طبقات أخرى، أهمها:
- حد معدل الطلبات Rate Limiting.
- الحظر التلقائي للعناوين التي تكثر الأخطاء Bad Behavior.
- قوائم الحظر والسماح، وصفحة تحقق Antibot.
- واجهة إدارة من المتصفح.
والرخصة AGPL-3.0، مع نسخة PRO مدفوعة فيها إضافات. وفي الإصدار 1.6.15 الذي شغلناه وجدنا داخله libmodsecurity 3.0.16 وقواعد CRS 4.29.0.
- نقاط القوة: كل الطبقات في Container واحد بإعدادات تكتبها كمتغيرات بيئة، وواجهة إدارة، وإعدادات افتراضية متشددة لمن لا يريد أن يضبط شيئاً.
- نقاط الضعف: هذه الإعدادات الافتراضية نفسها حجبت 39 من 45 رسالة مشروعة في تجربتنا كما سيأتي، والحزمة الكاملة استهلكت قرابة 1 GB من الذاكرة في التجربة.
وقد شرحنا مكان BunkerWeb بين البدائل المفتوحة للحلول التجارية في حلول الأمن التجارية وبدائلها.
أداة SafeLine
هي WAF من شركة Chaitin الصينية، ولا يعتمد على قواعد CRS، وإنما على محرك تحليل دلالي Semantic Analysis يحاول أن يفهم بنية SQL أو JavaScript داخل الطلب بدل البحث عن أنماط نصية. وتعرض الشركة في صفحة المستودع مقارنة بأرقامها هي، فيها نسبة إنذارات خاطئة 0.07% مقابل 17.58% ل ModSecurity على المستوى 1، وهذه أرقام من البائع على مجموعة اختبار من اختياره، فلا تأخذها كنتيجة مستقلة. والإصدار المجاني اسمه Personal ويسمح بعشرة تطبيقات، ثم Lite بعشرة دولارات في الشهر، ثم Pro بمئة دولار في الشهر، بحسب صفحة الأسعار عند كتابة المقال.
والسؤال المهم هنا: ما المفتوح فعلاً؟ المستودع على GitHub برخصة GPL-3.0، ولكن عندما تصفحنا ملفاته وجدنا فيه لوحة الإدارة وإضافات Kong وIngress-NGINX وأدوات مساعدة، ولم نجد فيه كود خدمة الفحص safeline-detector التي تحتوي المحرك الدلالي، وإنما يصل إليك هذا المحرك كـ Docker Image جاهز. وعندما ثبتنا الإصدار 9.4.2 وفتحنا لوحة الإدارة لأول مرة، ظهرت اتفاقية ترخيص قبل الدخول، والصورة التالية تبين أولها:

وفي نص الاتفاقية لاحظ التالي:
- الترخيص لتثبيت البرنامج وتشغيله على جهاز واحد، وهو غير حصري وغير قابل للنقل وقابل للإلغاء.
- يمنع النص الهندسة العكسية Reverse Engineering ومحاولة الوصول إلى الكود المصدري، ويمنع تقديم خدمة سحابية تجارية مبنية على البرنامج.
- يذكر النص أن الشركة تجمع بعض البيانات أثناء الاستخدام، مثل إصدار البرنامج واللغة وعنوان IP، لمعرفة الإصدار المستخدم ومدى استخدام الخصائص.
وهذا يعني أن SafeLine ليس برنامجاً مفتوح المصدر بالمعنى الذي تفهمه من رخصة GPL، وإنما منتج مجاني الاستخدام بشروط، وجزء منه فقط منشور كمصدر مفتوح. ولم نوافق على الاتفاقية نيابة عن أحد في المختبر، لذلك لم نكمل تجربة الفحص عليه، وحذفنا التثبيت.
- نقاط القوة: واجهة إدارة جيدة، ومحرك لا يعتمد على التعبيرات النمطية وحدها، وصفحة تحقق للروبوتات ومصادقة أمام التطبيق في نفس المنتج.
- نقاط الضعف: المحرك مغلق، والاستخدام مشروط باتفاقية يمكن أن تتغير كما تنص هي نفسها، والتثبيت الرسمي يشغل ال Container الرئيسي بشبكة المضيف Host Network ويربط مجلد
/var/runمن المضيف داخل Container الإدارة.
التجربة: نفس الهجمات ونفس الرسائل العربية
الصورة التالية تبين ترتيب المختبر: تطبيق واحد يرد على كل طلب، وأمامه ثلاثة جدران، وجهاز اختبار يرسل نفس الطلبات إلى كل جدار وإلى التطبيق مباشرة:

والمكونات كما يلي، وكلها على شبكة Docker واحدة اسمها lab-edge1-net ومنافذها على 127.0.0.1 فقط:
| المكون | الإصدار | دوره |
|---|---|---|
| التطبيق | traefik/whoami:v1.12.0 | يرد بالرمز 200 على أي طلب |
| محرك ModSecurity | owasp/modsecurity-crs:4.30.0-nginx-alpine-202610051210 | فيه ModSecurity 3.0.17 وNginx 1.30.5 وCRS 4.30.0 |
| أداة BunkerWeb | bunkerity/bunkerweb-all-in-one:1.6.15 | فيه libmodsecurity 3.0.16 وCRS 4.29.0 |
| محرك Coraza | خادم Caddy 2.11.7 مع إضافة coraza-caddy 2.6.2 | فيه Coraza 3.8.1 وCRS 4.25.0 |
| جهاز الاختبار | python:3.13.16-slim | سكربت بمكتبة Python القياسية فقط |
وكل الجدران على مستوى التشدد 1 وحد النقاط 5، أي الإعداد الافتراضي.
قائمة الهجمات وقائمة الرسائل العربية
اخترنا الهجمات من مستودع SecLists برخصة MIT، وثبتنا النسخة على Commit رقم 97e8798 بتاريخ 12 أكتوبر 2026 حتى تستطيع أن تعيد التجربة على نفس الملفات:
- Generic-SQLi.txt: 268 سطراً من حقن SQL، نرسلها في المعامل
qمن الرابط/search. - XSS-BruteLogic.txt: 113 سطراً من حقن السكربتات XSS، في نفس المعامل.
- LFI-Jhaddix.txt: 930 سطراً من محاولات قراءة الملفات Path Traversal، نرسلها في المعامل
fileمن الرابط/download.
أما الرسائل العربية فكتبناها بأنفسنا لتشبه ما يصل إلى نموذج اتصال متجر سعودي: 40 رسالة عادية فيها أسماء ومدن وأرقام طلبات وعلامات ترقيم عربية وأقواس «» وروابط وبريد إلكتروني، ثم 5 أسئلة من موقع تعليمي برمجي، فيها جملة SQL أو وسم <script> أو أمر Linux، لأن هذا ما يكتبه طلاب البرمجة فعلاً. وكل رسالة ترسل كنموذج POST إلى /contact بالحقول name وcity وmessage.
والسكربت wafbench.py يحسب الطلب مرفوضاً إذا كان الرد أي رمز غير 200، لأن التطبيق خلف الجدران يرد بالرمز 200 على كل شيء، ويرسل الطلبات بالتتابع من اتصال واحد. وتجد السكربت والقوائم كاملة في ملفات التجربة، وهذا جزء العد منه:
def attacks(targets):
sets = {'sqli': ('/search?q=', load('sqli.txt')), 'xss': ('/search?q=', load('xss.txt')), 'lfi': ('/download?file=', load('lfi.txt'))}
for name, url in targets:
c = conn(url); row = {}
for cat, (prefix, lines) in sets.items():
codes = Counter(req(c, 'GET', prefix + enc(l)) for l in lines)
blocked = sum(v for k, v in codes.items() if k != 200)
row[cat] = (blocked, len(lines), dict(codes))
print(name, json.dumps(row, ensure_ascii=False))ونشغله من Container على نفس الشبكة:
docker run --rm --name lab-edge1-client --network lab-edge1-net \
-v "$PWD/payloads:/payloads:ro" -v "$PWD/wafbench.py:/wafbench.py:ro" \
python:3.13.16-slim python /wafbench.py attacks \
direct=http://lab-edge1-app modsec=http://lab-edge1-modsec:8080 bw=http://lab-edge1-bw:8080نتيجة الهجمات
والمخرج سوف يكون كما يلي، وقد شغلنا Coraza في خطوة منفصلة بنفس الأمر، ووضعنا سطره هنا تحت السطرين الآخرين:
direct {"sqli": [0, 268, {"200": 268}], "xss": [0, 113, {"200": 113}], "lfi": [1, 930, {"200": 929, "ERR": 1}]}
modsec {"sqli": [130, 268, {"200": 138, "403": 130}], "xss": [111, 113, {"403": 111, "200": 2}], "lfi": [774, 930, {"403": 773, "200": 156, "ERR": 1}]}
bw {"sqli": [130, 268, {"200": 138, "403": 130}], "xss": [111, 113, {"403": 111, "200": 2}], "lfi": [773, 930, {"403": 772, "200": 157, "ERR": 1}]}
coraza {"sqli": [130, 268, {"200": 138, "403": 130}], "xss": [111, 113, {"403": 111, "200": 2}], "lfi": [782, 930, {"403": 781, "200": 148, "ERR": 1}]}في المخرج أعلاه لاحظ التالي:
- سطر واحد من قائمة LFI لم يرسل أصلاً، حتى إلى التطبيق مباشرة، وهو
ERR، لأن مكتبة Python ترفض إرساله كرابط، لذلك نحسب النسب من 929 سطراً. - الجدران الثلاثة متطابقة تقريباً، وهذا متوقع لأنها تنفذ نفس القواعد، والفرق الصغير في LFI بين 772 و781 يأتي من اختلاف إصدار CRS وطريقة كل محرك في فك الترميز.
- نسبة SQL Injection تبدو منخفضة، 130 من 268 أي 48.5%، والسبب أن القائمة فيها أسطر كثيرة ليست هجوماً كاملاً، مثل كلمة
updateوحدها أو الرمز#وحده، وقد تركناها كما هي لأننا لا نختار من القائمة ما يناسب النتيجة.
والجدول التالي يلخص النتيجتين، الهجمات والرسائل العربية، وسوف نشرح عمود الرسائل في الفقرة التالية:
| الجدار | حقن SQL من 268 | حقن XSS من 113 | قراءة الملفات من 929 | رسائل عربية عادية مرفوضة من 40 | أسئلة برمجية مرفوضة من 5 |
|---|---|---|---|---|---|
| محرك ModSecurity | 130 | 111 | 773 | 3 | 3 |
| أداة BunkerWeb بعد تعطيل الحد والحظر | 130 | 111 | 772 | 3 | 3 |
| محرك Coraza | 130 | 111 | 781 | 3 | 3 |
والصورة التالية تعرض نفس الأرقام كنسب:

الرسائل العربية: ثلاث رسائل عادية رفضت
شغلنا نفس السكربت بالوضع legit، والمخرج سوف يكون كما يلي، وقد نقلنا سطر BunkerWeb من نفس التشغيل إلى فقرته أدناه:
direct blocked 0/45 []
modsec blocked 6/45 [(14, 403), (22, 403), (33, 403), (42, 403), (43, 403), (44, 403)]والرسائل 42 و43 و44 هي أسئلة البرمجة التي فيها cat /etc/passwd و<script> وif (a > 5 && b < 10)، وهذه حالة الويكي نفسها التي شرحناها في دليل الطبقات. ولاحظ أن السؤالين عن select * from orders وunion all مرا دون مشكلة. أما الرسائل 14 و22 و33 فهي رسائل عادية تماماً:
- الرسالة 14 من «أمل الجهني»: «أرغب في إلغاء الاشتراك الشهري ابتداءً من الشهر القادم.»
- الرسالة 22 من «أسماء النفيعي» تسأل عن المقاسات.
- الرسالة 33 من «زياد الفيفي» تسأل عن التقسيط.
ولمعرفة السبب نقرأ سجل التدقيق Audit Log الذي يكتبه ال Image بصيغة JSON إلى مخرج ال Container، والأمر التالي يستخرج من كل طلب مرفوض على /contact رقم القاعدة ورسالتها وأول جزء من البيانات التي طابقتها:
docker logs lab-edge1-modsec 2>/dev/null | grep '^{"transaction' | grep '/contact' | python3 -c '
import sys, json
for line in sys.stdin:
t = json.loads(line)["transaction"]
for m in t["messages"]:
d = m["details"]; print(d["ruleId"], m["message"][:70], "|", d["data"][:120])'والمخرج للرسائل الثلاث العادية سوف يكون كما يلي، وقد حذفنا منه أسطر الأسئلة البرمجية، واختصرنا المسافات في بداية الأسطر، والقيم كما ظهرت في السجل:
942100 SQL Injection Attack Detected via libinjection | Matched Data: sos1s found within ARGS:message: #1:( AJ %D:'! 'D'4*1'C 'D4G1J '(*/'!K EF 'D4G1 'DB'/E.
949110 Inbound Anomaly Score Exceeded (Total Score: 5) |
942100 SQL Injection Attack Detected via libinjection | Matched Data: sos found within ARGS:name: #3E'! 'DFAJ9J
949110 Inbound Anomaly Score Exceeded (Total Score: 5) |
942100 SQL Injection Attack Detected via libinjection | Matched Data: sos found within ARGS:name: 2J'/ 'DAJAJ
949110 Inbound Anomaly Score Exceeded (Total Score: 5) |لاحظ أن القاعدة واحدة في الحالات الثلاث، وهي 942100 التي تستخدم مكتبة libinjection لاكتشاف حقن SQL، ولاحظ أن البيانات التي فحصتها ليست الاسم العربي، وإنما رموز إنجليزية غريبة: الاسم «أسماء النفيعي» أصبح #3E'! 'DFAJ9J، وفيه علامة اقتباس مفردة ' وعلامة !، وهذه بالضبط الرموز التي تبحث عنها المكتبة في حقن SQL.
لماذا أصبح الاسم «أسماء» رموزاً؟
القاعدة 942100 تطبق قبل الفحص ثلاثة تحويلات هي t:utf8toUnicode,t:urlDecodeUni,t:removeNulls. والتحويل الأول يحول كل حرف إلى صيغة %uHHHH، فحرف الألف «ا» رقمه في Unicode هو U+0627 فيصبح %u0627. ثم يأتي التحويل الثاني، ودليل ModSecurity يصفه بأنه يستخدم البايت المنخفض فقط ويصفر البايت العالي، إلا في مجال الحروف الإنجليزية عريضة العرض Full-width، فيصبح %u0627 هو البايت 0x27، وهذا البايت هو علامة الاقتباس المفردة ' في ASCII. والصورة التالية تبين ذلك:

وكل الحروف العربية تقع في المجال من U+0600 إلى U+06FF، لذلك يتحول كل حرف عربي إلى رمز ASCII ثابت، والجدول التالي يبين بعضها:
| الحرف | رقمه | يصبح بعد التحويل |
|---|---|---|
| الألف «ا» | U+0627 | علامة الاقتباس ' |
| الهمزة «ء» | U+0621 | علامة التعجب ! |
| الهمزة على الألف «أ» | U+0623 | الرمز # |
| الدال «د» | U+062F | الشرطة المائلة / |
| الباء «ب» | U+0628 | القوس ( |
| التاء «ت» | U+062A | النجمة * |
وبالتالي فكل كلمة فيها ألف تحمل علامة اقتباس في نظر القاعدة، وكلمة «ابتداءً» أصبحت '(*/'!K، أي اقتباس وقوس ونجمة وشرطة. والمكتبة libinjection تقسم النص إلى رموز Tokens وتقارن تسلسلها ببصمات حقن SQL معروفة، فإذا صادف أن التسلسل يشبه بصمة، كما في sos هنا، رفضت الطلب. ولهذا لا تفشل كل الرسائل العربية، وإنما بعضها بحسب ترتيب الحروف، وهذا أسوأ من الفشل الدائم، لأنه يظهر عند زبون واحد اسمه «أسماء» ولا يظهر عند زميلك الذي يجرب النموذج.
وقد يتساءل البعض: هل هذه مشكلة في ModSecurity وحده؟ والإجابة لا، فالسطر الذي رفضه Coraza في سجله هو نفسه حرفياً، القاعدة 942100 والبيانات #3E'! 'DFAJ9J، لأن Coraza ينفذ نفس التحويلات بنفس المعنى حتى تبقى قواعد CRS متوافقة. والمشكلة معروفة لفريق CRS، ففي النقاش رقم 3793 عن حروف كيريلية تتحول إلى < و> ذكر أحد المشاركين أن الحرفين العربيين «ؼ» و«ؾ» يتحولان بنفس الطريقة، والنقاش مفتوح حتى كتابة المقال، والحل المقترح فيه هو الاستثناء المحدد.
الإصلاح: استثناء ضيق لقاعدة واحدة
الحل البسيط هو رفع حد النقاط أو تعطيل القاعدة 942100، وكلاهما يزيل الحماية من حقن SQL في كل التطبيق لتصلح نموذجاً واحداً. والحل الأنسب هو أن تزيل هذه القاعدة وحدها، عن هذين الحقلين وحدهما، في هذا المسار وحده. والملف REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf سوف يكون كما يلي:
# Arabic text in the contact form: libinjection (942100) sees the low bytes left by
# t:utf8toUnicode,t:urlDecodeUni, so remove only this rule, only for these fields, only on /contact.
SecRule REQUEST_URI "@beginsWith /contact" \
"id:1001,phase:1,pass,nolog,\
ctl:ruleRemoveTargetById=942100;ARGS:name,\
ctl:ruleRemoveTargetById=942100;ARGS:message"ثم نعيد تشغيل ال Container مع هذا الملف:
docker rm -f lab-edge1-modsec
docker run -d --name lab-edge1-modsec --network lab-edge1-net \
-e BACKEND=http://lab-edge1-app:80 -e BLOCKING_PARANOIA=1 -p 127.0.0.1:18101:8080 \
-v "$PWD/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:/etc/modsecurity.d/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:ro" \
owasp/modsecurity-crs:4.30.0-nginx-alpine-202610051210ونعيد الرسائل والهجمات، والمخرج سوف يكون كما يلي:
modsec-fixed blocked 3/45 [(42, 403), (43, 403), (44, 403)]
modsec-fixed {"sqli": [130, 268, {"200": 138, "403": 130}], "xss": [111, 113, {"403": 111, "200": 2}], "lfi": [774, 930, {"403": 773, "200": 156, "ERR": 1}]}في المخرج أعلاه لاحظ التالي:
- الرسائل العربية الأربعون مرت كلها، وبقيت الأسئلة البرمجية الثلاثة مرفوضة لأن قواعد XSS وحقن الأوامر ما زالت تعمل على نفس الحقول، فإذا كان موقعك تعليمياً فاستثناؤها قرار منفصل كما في مثال الويكي في دليل الطبقات.
- نتيجة الهجمات على
/searchو/downloadلم تتغير رقماً واحداً، لأن الاستثناء لا يلمس غير/contact. - القاعدة
942100ما زالت تفحص كل الحقول الأخرى في/contactنفسه، وما زالت قواعد SQL الأخرى المبنية على التعبيرات النمطية تفحص الاسم والرسالة.
DetectionOnly، ثم ابحث في السجل عن القاعدة 942100 في الحقول التي يكتب فيها الناس بالعربية، كالاسم والعنوان والتعليق، والسبب أن هذه الإنذارات الخاطئة لا تظهر في اختباراتك بالإنجليزية، وإنما عند زبائن حقيقيين يتركون النموذج دون أن يخبروك.الإعدادات الافتراضية في BunkerWeb
في التشغيل الأول ل BunkerWeb لم نغير إلا عنوان التطبيق، فكانت النتيجة على الرسائل كما يلي:
bw blocked 39/45 [(3, 429), (4, 429), (5, 429), (6, 429), (7, 429), (8, 429), (11, 429), (12, 429), (13, 429), (14, 429), (15, 429), (16, 429), (17, 429), (18, 429), (19, 429), (22, 429), (23, 429), (24, 429), (25, 429), (26, 429), (27, 429), (28, 429), (29, 429), (30, 403), (31, 403), (32, 403), (33, 403), (34, 403), (35, 403), (36, 403), (37, 403), (38, 403), (39, 403), (40, 403), (41, 403), (42, 403), (43, 403), (44, 403), (45, 403)]وفي سجل ال Container السبب الأول واضح، وهذا جزء من السطر:
[warn] 721#721: *147 [ACCESS] denied access from limit : client IP 192.168.80.5 is limited for URL /contact (current rate = 3r/s and max rate = 2r/s)وعندما قرأنا القيم الافتراضية من ملفات plugin.json داخل ال Image وجدنا التالي:
| الإعداد | القيمة الافتراضية | ماذا يعني |
|---|---|---|
USE_LIMIT_REQ وLIMIT_REQ_RATE | مفعل، 2r/s | طلبان في الثانية لكل عنوان على كل رابط، ثم الرمز 429 |
USE_BAD_BEHAVIOR | مفعل | يعد الردود 400 و401 و403 و404 و405 و429 و444 لكل عنوان |
BAD_BEHAVIOR_THRESHOLD | 10 خلال 60 ثانية | بعدها يحظر العنوان كله |
BAD_BEHAVIOR_BAN_TIME | 86400 | الحظر يوم كامل |
USE_BUNKERNET | مفعل | يشارك بيانات التهديدات مع نسخ BunkerWeb الأخرى عبر خدمة BunkerNet التابعة للشركة، كما يصفه ملف الإضافة |
والنتيجة أن السكربت تجاوز حد الطلبين في الثانية، فحصل على الرمز 429 أكثر من عشر مرات، فاعتبره BunkerWeb سيئ السلوك وحظر عنوانه يوماً كاملاً، فرفضت كل الطلبات بعد الرسالة 29 بالرمز 403. وقد يتساءل البعض: ولكن الزبون لا يرسل 45 رسالة في ثوان؟ والإجابة صحيحة، ولكن تذكر أن كل من خلف نفس عنوان NAT يحسب كعميل واحد، كموظفي شركة أو طلاب جامعة، وأن تطبيق جوال يستدعي ال API عدة مرات عند الفتح قد يتجاوز طلبين في الثانية وحده، وقد شرحنا هذه المسألة في أين تضع الحد، وبأي مفتاح. لذلك راجع هذه القيم قبل النشر، ولا تتركها كما هي على واجهات API.
ولقياس القواعد وحدها أعدنا تشغيله بالمتغيرات USE_LIMIT_REQ=no وUSE_BAD_BEHAVIOR=no وUSE_BUNKERNET=no، فأصبحت نتيجته مطابقة ل ModSecurity، وهي الأرقام التي في الجدول أعلاه.
كم يضيف الجدار من التأخير؟
أرسلنا نفس الرسالة العربية 500 مرة بالتتابع إلى كل هدف، بعد 50 طلباً للتسخين، وقسنا زمن كل طلب من جهاز الاختبار:
direct n=500 p50=0.89ms p95=18.34ms p99=87.34ms last_status=200
modsec n=500 p50=32.73ms p95=175.44ms p99=334.37ms last_status=200
bw n=500 p50=35.59ms p95=306.13ms p99=1398.41ms last_status=200
coraza n=500 p50=16.62ms p95=472.69ms p99=4594.77ms last_status=200في المخرج أعلاه لاحظ التالي:
- الوسيط p50 هو الرقم الأكثر ثباتاً هنا: الطلب المباشر أقل من 1 ملي ثانية، ومع الجدار بين 17 و36 ملي ثانية، أي أن فحص الرسالة الواحدة على مئات القواعد يكلف عشرات الملي ثواني على هذا الجهاز.
- أرقام p95 وp99 مضطربة جداً، والسبب أن الجهاز كان مشتركاً مع تجارب أخرى تستهلك المعالج وقت القياس، لذلك لا تقارن الأدوات بها، ولا تعتبر أن Coraza أبطأ في الذيل من هذه التجربة وحدها.
- هذه أرقام توضيحية من جهاز واحد مزدحم، والقياس الصحيح لموقعك هو أن تشغل نفس الاختبار على سيرفرك بطلباتك الحقيقية، كما شرحنا في اختبار الحمل.
والخلاصة العملية أن الجدار يضيف تأخيراً ملموساً في كل طلب يحمل جسماً، وهذا مقبول على نماذج الاتصال وصفحات الدخول، ولكنه يستحق التفكير على واجهات API التي تستقبل آلاف الطلبات في الثانية.
إزالة التجربة
نحذف ما أنشأناه بالاسم فقط:
docker rm -f lab-edge1-app lab-edge1-modsec lab-edge1-bw lab-edge1-coraza
docker volume rm lab-edge1-bw-data
docker network rm lab-edge1-netأي جدار تختار؟
| حالتك | الأنسب | والسبب |
|---|---|---|
| تستخدم Nginx أو Nginx Proxy Manager أو Traefik | تشغيل Container من owasp/modsecurity-crs بين ال Proxy والتطبيق | لا تغير ال Proxy الحالي، وتحصل على أحدث CRS مباشرة |
| تستخدم Caddy | إضافة coraza-caddy | نفس القواعد داخل ال Proxy دون Container إضافي |
| تريد كل الطبقات جاهزة بواجهة إدارة | أداة BunkerWeb | حد المعدل والحظر وCRS معاً، بشرط أن تراجع القيم الافتراضية |
| تحتاج محركاً مختلفاً عن CRS وتقبل منتجاً مغلقاً بشروط | أداة SafeLine | اقرأ اتفاقية الترخيص أولاً، ولا تعتبره مفتوح المصدر |
| موقعك خلف Cloudflare أصلاً | قواعد Cloudflare في الخارج وجدار صغير في الداخل | كما شرحنا في خدمات Cloudflare وبدائلها |
وأياً كان اختيارك، فالجدار طبقة تؤخر المهاجم وتخفف الضجيج، ولا تغني عن إصلاح الثغرة في الكود نفسه، ولا عن فحص التطبيق من الخارج كما في فحص DAST مع ZAP، ولا عن طبقات الحماية من الإغراق في طبقات الحماية من DDoS.
أخطاء شائعة
- الاختبار بالإنجليزية وحدها: كل الهجمات في القوائم العامة إنجليزية، فأضف رسائل عربية حقيقية من نماذجك إلى اختبارك، فهي ما كشف المشكلة هنا.
- رفع حد النقاط لإسكات الإنذارات: إذا رفعت الحد من 5 إلى 10 فإن هجوماً يطابق قاعدة واحدة حرجة سوف يمر، فأصلح القاعدة التي تخطئ بدل أن تضعف الجدار كله.
- الاستثناء بالأمر
SecRuleRemoveByIdللتطبيق كله: يزيل القاعدة من كل الحقول وكل المسارات، فاستخدمctl:ruleRemoveTargetByIdبمسار وحقل محددين. - ترك القيم الافتراضية في BunkerWeb على واجهة API: طلبان في الثانية ثم حظر يوم كامل قد يوقف تطبيق الجوال عند كل الزبائن خلف شبكة واحدة.
- اعتبار المستودع المفتوح منتجاً مفتوحاً: اقرأ ما يوجد فعلاً في المستودع وما يصلك كـ Docker Image مغلق، واقرأ الاتفاقية التي تظهر عند التثبيت.
- ترك الجدار يرد على التطبيق وحده: إذا كان منفذ التطبيق مفتوحاً على السيرفر فالمهاجم يتجاوز الجدار كله، فاجعل التطبيق على شبكة داخلية لا يصلها إلا الجدار، كما في إصلاح الطريق المباشر.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- ثلاثة من الخيارات الأربعة تنفذ نفس قواعد CRS، لذلك جاءت نسب الاكتشاف في تجربتنا متقاربة جداً، والفرق الحقيقي بينها في طريقة التشغيل والإدارة والإعدادات الافتراضية.
- ثلاث رسائل عربية عادية من أربعين رفضتها الجدران الثلاثة على المستوى 1، والسبب أن التحويل
urlDecodeUniيحول كل حرف عربي إلى رمز ASCII، فتصبح الألف علامة اقتباس في نظر القاعدة942100. - الإصلاح استثناء ضيق لقاعدة واحدة في حقول ومسار محددين، وبعده مرت الرسائل الأربعون ولم تتغير نتيجة الهجمات.
- القيم الافتراضية في BunkerWeb حجبت 39 من 45 رسالة مشروعة بسبب حد الطلبات والحظر التلقائي، فراجعها قبل النشر.
- أداة SafeLine منتج مجاني بشروط وليس مفتوح المصدر بالكامل، فمحرك الفحص مغلق والاستخدام خاضع لاتفاقية ترخيص تظهر عند أول دخول.
دليلحماية تطبيقات الويب بطبقات أمان في ال Reverse Proxyكلمة المرور وحدها لا تحمي تطبيقاً مكشوفاً على الإنترنت، لذلك نضع أمامه طبقات حماية في ال Reverse Proxy نفسه.
دليلخدمات Cloudflare بكلمات بسيطة وبدائلها مفتوحة المصدرما الذي تقدمه كل خدمة من Cloudflare، وما المتاح منها مجاناً، وما البديل مفتوح المصدر الذي تشغله على سيرفرك.
دليلطبقات الحماية من هجمات DDoS وأين تقف كل طبقةكيف تبني دفاعاً بالطبقات ضد DDoS من المزود وال CDN إلى ضبط النواة والتخزين المؤقت، ومتى لا يكفي سيرفرك وحده.
دليلأدوات الحماية من DDoS بين الخدمات المدارة وسيرفركماذا تقدم خدمات الحماية المدارة وبكم، وكيف تبني طبقة دفاع على سيرفرك وتقيس أثرها قبل الهجوم وبعده.
دليلتحديد معدل الطلبات Rate Limiting وخوارزمياتهكيف تعمل خوارزميات تحديد معدل الطلبات، وأيها يناسب واجهتك، وكيف تطبقها على عدة سيرفرات وترد بالرمز 429 الصحيح.
دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Docker Engine 29.8.0 مع ModSecurity 3.0.17 وCRS 4.30.0 (owasp/modsecurity-crs:4.30.0-nginx-alpine-202610051210) وBunkerWeb 1.6.15 وCaddy 2.11.7 مع coraza-caddy 2.6.2 وCoraza 3.8.1، وقوائم SecLists على Commit رقم 97e8798، وتثبيت SafeLine 9.4.2 دون إكمال التجربة.