> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# OWASP ASVS ومراجعة الكود الأمنية لكل Pull Request
- URL: https://arabroot.io/articles/متطلبات-الامان-ومراجعة-الكود-مع-owasp-asvs/
- Published: 2026-10-06T03:45:00.000Z
- Updated: 2026-10-14T07:41:22.000Z
- Description: كيف تكتب متطلبات أمان يمكن اختبارها مع OWASP ASVS، وتراجع كل طلب دمج أمنياً في عشرين دقيقة بقائمة جاهزة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, الأمن السيبراني, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك تذكرة Ticket في نظام المهام عنوانها «إضافة استعادة كلمة المرور وعرض الفاتورة الواحدة»، وفي آخرها سطر يقول «مع مراعاة الأمان»، وبعد يومين وصل طلب الدمج Pull Request وفيه ثلاثون سطراً، فقرأه زميلك وهو بين اجتماعين، وتأكد أن الأسماء واضحة وأن الاختبارات خضراء، ثم كتب «LGTM» ودمجه. لا أحد في هذه القصة أهمل عمله، ولكن لا أحد سأل السؤال الذي يهم المهاجم: هل يستطيع عميل أن يقرأ فاتورة عميل آخر بتغيير رقم في الرابط؟ والسبب أن «مع مراعاة الأمان» ليست متطلباً يمكن أن ينجح أو يفشل، ومراجعة الكود Code Review لم يكن فيها سؤال أمني واحد مكتوب.

في دليل [نمذجة التهديدات Threat Modeling](https://arabroot.io/articles/%D9%86%D9%85%D8%B0%D8%AC%D8%A9-%D8%A7%D9%84%D8%AA%D9%87%D8%AF%D9%8A%D8%AF%D8%A7%D8%AA-threat-modeling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/) تحدثنا عن السؤال «ما الذي قد يسوء؟» على التصميم قبل الكود، وفي دليل [DevSecOps للمبتدئين](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/) رأينا الأدوات التي تعمل في خط CI/CD وما يجده كل منها. وهذا المقال يغطي ما بينهما: كيف تكتب متطلبات أمان Security Requirements يمكن اختبارها، وكيف يراجع الإنسان طلب الدمج أمنياً في وقت معقول. وسوف نناقش في هذا المقال ما يلي:

- لماذا لا تصلح جملة «اجعله آمناً» متطلباً، وما الذي يجعل المتطلب الأمني قابلاً للتحقق Verifiable.
- معيار OWASP ASVS في إصداره الحالي 5.0.0: الفصول السبعة عشر والمستويات الثلاثة، وكيف تختار المستوى المناسب لتطبيقك.
- تحويل المتطلب إلى قصة مستخدم User Story ومعايير قبول Acceptance Criteria واختبار، مع سكربت صغير يختار متطلبات الميزة من ملف ASVS الرسمي.
- أين يقع ASVS بجانب ISO 27001 وضوابط NCA ECC، ومتى تحتاج MASVS للجوال وSAMM لنضج البرنامج وSSDF.
- مراجعة الكود الأمنية Security Code Review: ما يراه الإنسان ولا تراه الأدوات، وطريقة مراجعة طلب دمج في عشرين دقيقة، وملف CODEOWNERS وسفراء الأمن Security Champions.
- قائمة مراجعة أمنية جاهزة للنسخ، وملف قالب طلب الدمج PR Template في Gitea وGitHub.
- مثال عملي حقيقي: طلب دمج فيه خمس مشكلات، وتعليقات المراجعة، ثم ما وجدته Semgrep وOSV-Scanner والاختبارات وما لم يجده إلا الإنسان، ثم الكود بعد الإصلاح.

## لماذا لا تصلح جملة «اجعله آمناً» Make it secure متطلباً؟

المتطلب Requirement في أي مشروع هو جملة يستطيع شخص آخر أن يتحقق منها فيقول «نجح» أو «فشل»، فجملة «الصفحة تعرض الفواتير مرتبة بالتاريخ» متطلب لأن المختبر يفتح الصفحة ويرى الترتيب، أما «اجعله آمناً» فلا يستطيع أحد أن يحكم عليها، لأن كل شخص يفهمها بحسب ما يعرفه، والمطور الذي لا يعرف ثغرة الوصول المباشر إلى الكائنات IDOR لن يمنعها مهما كان حريصاً. وهذه الحلول التي تخطر على البال أولاً:

1. **سطر «مع مراعاة الأمان» في التذكرة:** لا أحد يعرف ماذا يختبر، ولا متى تكون المهمة منتهية من جهة الأمان، فينتهي السطر إلى لا شيء.
2. **قائمة OWASP Top 10 كمتطلبات:** القائمة [وثيقة توعية Awareness Document](https://top10.owasp.org/2025/?ref=arabroot.io) بأشهر فئات المخاطر، وقد شرحناها في [OWASP Top 10 للمبتدئين](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، ولكن «لا يوجد كسر في التحكم بالوصول Broken Access Control» ليست جملة تختبرها، لأنها تصف فئة كاملة من الأخطاء ولا تقول ما الذي يجب أن يكون صحيحاً في تطبيقك.
3. **الاعتماد على أدوات الفحص في CI:** أداة الفحص الثابت SAST تجد أنماطاً في الكود، ولكنها لا تعرف أن الفاتورة يجب أن تكون لمستأجر Tenant المستخدم نفسه، لأن هذه قاعدة عمل Business Rule في رأسك وليست في الكود، كما شرحنا في [ما لا تراه أداة SAST أبداً](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/#blind-spots).
4. **اختبار الاختراق Penetration Test قبل الإطلاق:** يأتي متأخراً ولوقت محدود، ويجد ما يستطيع الوصول إليه في أيام، ولا يعطيك قائمة بما يجب أن يكون صحيحاً في كل ميزة جديدة.

والحل الأنسب هو أن تكتب المتطلب الأمني بالطريقة نفسها التي تكتب بها المتطلب الوظيفي: جملة محددة، لها رقم، يمكن أن تنجح أو تفشل، ولها اختبار. ولأن كتابة مئات الجمل من الصفر عمل كبير وسهل الخطأ، فالأفضل أن تبدأ من معيار كتبه مجتمع كامل وراجعه، وهنا يأتي ASVS.

## ما هو OWASP ASVS؟ معيار متطلبات أمان Security Requirements يمكن التحقق منها

معيار التحقق من أمان التطبيقات Application Security Verification Standard واختصاراً ASVS هو مشروع من OWASP يعرف نفسه في [فصل «What is the ASVS?»](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x03-What-is-the-ASVS.md?ref=arabroot.io) بأنه يحدد متطلبات الأمان لتطبيقات الويب والخدمات، ويشترط في كل متطلب أن يكون له أثر أمني واضح، وأن يكون قابلاً للتحقق بنتيجة «pass» أو «fail». وكل متطلب يبدأ بكلمة «Verify that»، أي «تحقق من أن…»، ولاحظ أن المعيار يحتوي متطلبات فقط (must) ولا يحتوي توصيات (should)، وهذا بالضبط ما ينقص سطر «مع مراعاة الأمان».

الإصدار الحالي هو ASVS 5.0.0، وقد نشر في [مستودع المشروع على GitHub](https://github.com/OWASP/ASVS/releases/tag/v5.0.0%5Frelease?ref=arabroot.io) في 30 مايو 2025، وتقول صفحة الحقوق فيه «Version 5.0.0, May 2025»، وترخيصه Creative Commons Attribution-ShareAlike 4.0، ويأتي بصيغ PDF وCSV وJSON وغيرها. وهو إصدار رئيسي Major Release أعاد ترتيب كل شيء، فبحسب [فصل التغييرات عن الإصدار 4](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x05-For-Users-Of-4.0.md?ref=arabroot.io) كان في الإصدار 4.0.3 عدد 286 متطلباً، بقي منها 11 فقط دون أي تغيير، وأزيل أو دمج 109 متطلبات، لذلك إذا كانت مؤسستك تستخدم أرقام الإصدار 4 فاستخدم جداول الربط Mapping التي ينشرها المشروع ولا تفترض أن الرقم نفسه يعني المتطلب نفسه.

وقد يتساءل البعض: إذا كانت الأرقام تتغير بين الإصدارات، فكيف أكتب رقم المتطلب في التذكرة؟ والإجابة أن المعيار نفسه يقترح الصيغة `v<version>-<chapter>.<section>.<requirement>`، فالرقم `v5.0.0-8.2.2` يعني الفصل 8 (Authorization) والقسم 2 والمتطلب 2 من الإصدار 5.0.0 بالتحديد، وهذه الصيغة هي التي سوف نستخدمها في التذاكر والكود والاختبارات في هذا المقال.

وهناك فكرة جديدة في الإصدار 5 تستحق الانتباه، وهي القرارات الأمنية الموثقة Documented Security Decisions: بعض المتطلبات لا يمكن أن يحددها المعيار لكل التطبيقات، مثل قواعد الصلاحيات وأنواع الملفات المسموح برفعها، فبدلاً من جملة عامة مثل «كل البيانات يجب أن تشفر»، يطلب المعيار في أول قسم من الفصل أن تكون القواعد مكتوبة، مثل المتطلب 8.1.1 الذي يطلب توثيق قواعد الوصول على مستوى الوظيفة والبيانات، ثم يأتي متطلب تنفيذ يتحقق من أن الكود يطبق ما كتب. وبالتالي يصبح التحقق نشاطين منفصلين: هل القاعدة مكتوبة؟ وهل الكود يطبقها؟

⚠️

لا توجد «شهادة ASVS» رسمية، فالمعيار يقول في [فصل التقييم والشهادات](https://github.com/OWASP/ASVS/blob/v5.0.0/5.0/en/0x04-Assessment%5Fand%5FCertification.md?ref=arabroot.io) إن OWASP لا تعتمد أي مورد أو مدقق أو برنامج، وإن أي علامة أو شهادة تدعي التوافق مع ASVS ليست معتمدة من OWASP. لذلك إذا عرض عليك أحد «شهادة ASVS» فاسأله عن النطاق وطريقة الاختبار والمتطلبات التي فحصت، ولا تتعامل معها كختم رسمي.

### الفصول Chapters السبعة عشر: ماذا يغطي كل فصل؟

يتكون الإصدار 5.0.0 بحسب المقدمة من نحو 350 متطلباً في 17 فصلاً، وعندما عددنا الصفوف في ملف CSV الرسمي وجدنا 345 متطلباً بالضبط. والهدف من التقسيم أن تستبعد ما لا يخصك بسهولة، فواجهة API بين خدمتين لا يلزمها فصل أمان الواجهة الأمامية V3، والتطبيق الذي لا يستخدم OAuth أو WebRTC يتجاهل فصليهما. والجدول التالي يلخص الفصول مع عدد المتطلبات:

| الفصل | الاسم في المعيار                    | ماذا يغطي                                                            | المتطلبات (L1 / L2 / L3) |
| ----- | ----------------------------------- | -------------------------------------------------------------------- | ------------------------ |
| V1    | Encoding and Sanitization           | الترميز Encoding ومنع الحقن Injection                                | 30 (8 / 19 / 3)          |
| V2    | Validation and Business Logic       | التحقق من المدخلات Input Validation ومنطق العمل ومنع الأتمتة المسيئة | 13 (4 / 7 / 2)           |
| V3    | Web Frontend Security               | أمان المتصفح: الترويسات وملفات تعريف الارتباط Cookies وغيرها         | 31 (8 / 11 / 12)         |
| V4    | API and Web Service                 | واجهات API وGraphQL وWebSocket                                       | 16 (2 / 8 / 6)           |
| V5    | File Handling                       | رفع الملفات وتخزينها وتنزيلها                                        | 13 (4 / 5 / 4)           |
| V6    | Authentication                      | المصادقة Authentication وكلمات المرور والعامل الثاني والاستعادة      | 47 (13 / 22 / 12)        |
| V7    | Session Management                  | إدارة الجلسات Sessions                                               | 19 (6 / 12 / 1)          |
| V8    | Authorization                       | الصلاحيات Authorization على الوظيفة والبيانات والحقول                | 13 (4 / 3 / 6)           |
| V9    | Self-contained Tokens               | التوكنات المكتفية بذاتها مثل JWT                                     | 7 (4 / 3 / 0)            |
| V10   | OAuth and OIDC                      | OAuth وOpenID Connect                                                | 36 (5 / 24 / 7)          |
| V11   | Cryptography                        | التشفير Cryptography والهاش والأرقام العشوائية Random Values         | 24 (3 / 11 / 10)         |
| V12   | Secure Communication                | TLS والاتصال بين الخدمات                                             | 12 (3 / 6 / 3)           |
| V13   | Configuration                       | الإعدادات Configuration وإدارة الأسرار Secrets                       | 21 (1 / 12 / 8)          |
| V14   | Data Protection                     | حماية البيانات الحساسة Data Protection                               | 13 (2 / 7 / 4)           |
| V15   | Secure Coding and Architecture      | البرمجة الدفاعية Defensive Coding والاعتماديات Dependencies والتزامن | 21 (3 / 10 / 8)          |
| V16   | Security Logging and Error Handling | السجلات الأمنية Security Logging ومعالجة الأخطاء Error Handling      | 17 (0 / 16 / 1)          |
| V17   | WebRTC                              | اتصالات الصوت والفيديو في المتصفح                                    | 12 (0 / 7 / 5)           |

الصورة التالية تبين الفصول والمستويات معاً، وعليها الفصول التي سوف يلمسها طلب الدمج في المثال العملي:

![على اليسار سبع عشرة بطاقة لفصول ASVS 5.0 من V1 Encoding and Sanitization إلى V17 WebRTC، في كل بطاقة شريط ملون بعدد متطلبات L1 بالأخضر وL2 بالأزرق وL3 بالبنفسجي، مثل V6 Authentication فيها 47 متطلباً منها 13 في L1 و22 في L2 و12 في L3، والفصول V1 وV8 وV11 وV15 وV16 محاطة بإطار برتقالي لأن طلب الدمج في المثال يلمسها. وعلى اليمين ثلاثة أشرطة تراكمية: L1 فيه 70 متطلباً وهو الحد الأدنى وخط الدفاع الأول، وL2 فيه 253 متطلباً وهو ما يجب أن تسعى إليه أغلب التطبيقات، وL3 فيه 345 متطلباً للضمان العالي ودفاع العمق، وتحتها شرح الترقيم v5.0.0-8.2.2](https://arabroot.io/content/images/2026/10/asvs-review-02-levels-chapters.webp)

كل مستوى يشمل ما قبله، والمستوى الذي تختاره يحدد أي الأشرطة تلزمك في كل فصل

### المستويات الثلاثة L1 وL2 وL3: ماذا تعني في الإصدار 5؟

تغير تعريف المستويات في الإصدار 5 عما كان في الإصدار 4، فهي الآن مستويات حسب الأولوية Priority-based، أي أن كل متطلب وضع في مستوى بحسب مقدار ما يقلله من الخطر مقارنة بالجهد المطلوب لتنفيذه، مع الحرص على أن يكون الدخول إلى المعيار سهلاً. والمستويات تراكمية، فالمستوى الثاني يعني كل متطلبات الأول والثاني:

- **المستوى الأول L1:** نحو 20% من المتطلبات (70 في ملف CSV)، وهي الحد الأدنى ونقطة البداية، وأغلبها خط الدفاع الأول ضد هجمات شائعة لا تحتاج ثغرة أخرى أو شرطاً مسبقاً. ولاحظ أن المعيار يقول إن هذا المستوى ليس بالضرورة قابلاً للفحص من الخارج دون الاطلاع على الكود أو التوثيق، لذلك من يتحقق منه يحتاج الكود أو التوثيق، وليس اختبار الصندوق الأسود Black Box من الخارج فقط.
- **المستوى الثاني L2:** نحو 50% إضافية (183 متطلباً)، فيصبح المطلوب نحو 70% من المعيار (253 متطلباً)، ويقول المعيار إن أغلب التطبيقات يجب أن تسعى إلى هذا المستوى. ومتطلباته تخص هجمات أقل شيوعاً أو حماية أكثر تعقيداً ضد الهجمات الشائعة.
- **المستوى الثالث L3:** آخر 30% تقريباً (92 متطلباً)، وأغلبها دفاع في العمق Defense in Depth أو ضوابط مفيدة لكنها صعبة التنفيذ، وهو هدف التطبيقات التي تريد أن تثبت أعلى مستوى من الأمان.

ولاحظ نتيجة مهمة في تطبيقنا: المتطلب 11.5.1 الذي يطلب أن تولد القيم غير القابلة للتخمين بمولد أرقام عشوائية آمن تشفيرياً CSPRNG بعشوائية 128 بت على الأقل هو متطلب من المستوى الثاني، والمتطلب 16.2.5 الذي يمنع كتابة البيانات الحساسة في السجلات من المستوى الثاني أيضاً، وفصل السجلات V16 كله ليس فيه أي متطلب من المستوى الأول. وهذا يعني أن تطبيقاً يكتفي بالمستوى الأول قد يمر بالمعيار وهو يكتب التوكنات في السجل، لذلك لا تختر المستوى بحسب سهولته، وإنما بحسب ما تحميه.

### كيف تختار المستوى Level المناسب لتطبيقك؟

المعيار لا يفرض مستوى على أحد، وإنما يقول إن المؤسسة يجب أن تحلل مخاطرها وتقرر بحسب حساسية التطبيق وتوقعات مستخدميه، ويضرب مثالين: شركة ناشئة تجمع بيانات حساسة قليلة قد تبدأ بالمستوى الأول، وبنك يصعب عليه أن يبرر لعملائه أقل من المستوى الثالث لتطبيق الخدمات المصرفية. ومن هذين المثالين نقترح الطريقة العملية التالية، وهي اقتراحنا وليست نص المعيار:

1. **ابدأ بقائمة التطبيقات** وصنف كل تطبيق بحسب البيانات التي يحملها والضرر إذا تسربت أو تغيرت: أداة داخلية بلا بيانات شخصية، تطبيق فيه بيانات عملاء أو بيانات شخصية أو فواتير، تطبيق مالي أو صحي أو جزء من بنية حرجة.
2. **اجعل المستوى الأول أرضية لكل شيء**، حتى الأداة الداخلية الصغيرة، لأن متطلباته خط الدفاع الأول ضد الهجمات الشائعة، والمعيار نفسه يقول إن التخصيص يبدأ من المستوى الأول ثم يرتفع بحسب الخطر.
3. **اجعل المستوى الثاني هدف أي تطبيق فيه بيانات عملاء** مثل تطبيق الفواتير في هذا المقال، فهو الذي يغطي العشوائية الآمنة والسجلات وكثيراً من تفاصيل المصادقة.
4. **خذ متطلبات من المستوى الثالث عند الحاجة** دون أن تلتزم بالمستوى كله، فالمعيار يسمح صراحة بأن تقدم متطلبات من مستوى أعلى بحسب مخاطرك.
5. **احذف الفصول التي لا تخصك واكتب السبب**، فالمعيار يشجع المؤسسات على نسخة مخصصة Fork تحذف ما لا تستخدمه مثل GraphQL أو WebSocket، بشرط أن يبقى الرقم 8.2.2 يعني الشيء نفسه في كل مكان.

## من المتطلب Requirement إلى قصة المستخدم User Story والاختبار Test

المتطلب الذي يبقى في ملف PDF لا يحمي أحداً، والمكان الطبيعي له هو المكان الذي يعمل فيه الفريق كل يوم: التذكرة ومعايير القبول والاختبار. والصورة التالية تبين كيف يمر متطلب واحد بخمس محطات، وكيف تعود كل ملاحظة إلى البداية:

![خمس بطاقات مرقمة من اليمين إلى اليسار يمر بها المتطلب ASVS v5.0.0-8.2.2 data-specific access: 1 Requirement المتطلب مكتوب ومرقم مع رسم وثيقة والنص invoice belongs to caller's tenant، 2 Design أين يقع الفحص في التصميم مع درع والنص tenant_id on every query، 3 Code الكود ينفذ المتطلب مع حاسوب والنص WHERE id=? AND tenant_id=?، 4 Test اختبار يثبت المتطلب مع علامة صح والنص tenant A يطلب فاتورة B فيحصل على 404، 5 Review مراجعة طلب الدمج مع فريق والنص checklist + CODEOWNERS، وسهم برتقالي متقطع يعود من المراجعة إلى المتطلب ومكتوب عليه كل ملاحظة في المراجعة أو في الإنتاج تعود متطلباً أو اختباراً](https://arabroot.io/content/images/2026/10/asvs-review-01-loop.webp)

الرقم نفسه يظهر في التذكرة والكود والاختبار وتعليق المراجعة، فتعرف دائماً لماذا كتب هذا السطر

ونمذجة التهديدات والمعيار يعملان معاً ولا يغني أحدهما عن الآخر: جلسة نمذجة التهديدات تجد ما هو خاص بتصميمك، مثل أن كل استعلام يجب أن يحمل رقم المستأجر، ومعيار ASVS يعطيك قائمة بما يجب أن يكون صحيحاً في كل تطبيق ويب، مثل أن التوكنات تولد بمولد آمن، والقائمة الثانية تذكرك بما قد تنساه الجلسة الأولى. والمثال التالي يبين كيف تبدو الميزة نفسها بعد أن تكتب بهذه الطريقة:

```markdown
## INV-142: View a single invoice, and request a password reset

User story
As a customer, I want to open one invoice by its number, and reset my password by email.

Security requirements (ASVS 5.0.0, target level L2)
- v5.0.0-8.2.2  An invoice is returned only if it belongs to the caller's tenant.
- v5.0.0-1.2.4  Every database query is parameterized.
- v5.0.0-11.5.1 The reset token comes from a CSPRNG with at least 128 bits.
- v5.0.0-16.2.5 Tokens, passwords and email addresses are never written to logs.
- v5.0.0-15.2.1 No new dependency with a known vulnerability past our fix deadline.

Acceptance criteria
- Given user A of tenant 1, when A requests an invoice of tenant 2, then the answer is 404.
- Given an email with a quote character, the reset request creates no token.
- After a reset request, the log contains neither the token nor the email.
- The reset answer is the same whether the email exists or not.

Abuse cases
- An attacker changes the invoice id in the URL.
- An attacker puts SQL in the email field.
- An attacker reads the application logs.
```

في التذكرة أعلاه لاحظ التالي:

- كل متطلب أمني يحمل رقمه في المعيار، فالمراجع يعرف ماذا يبحث، والمختبر يعرف ماذا يختبر.
- معايير القبول مكتوبة بصيغة Given/When/Then التي تتحول مباشرة إلى اختبار تكامل Integration Test، وهذه الاختبارات سوف تراها تعمل في المثال العملي.
- حالات الإساءة Abuse Cases هي الوجه الآخر لقصة المستخدم: ماذا يريد المهاجم أن يفعل بالميزة نفسها، وتشرح OWASP طريقة كتابتها في [Abuse Case Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Abuse%5FCase%5FCheat%5FSheet.html?ref=arabroot.io).
- الاختبار الأمني هنا اختبار عادي يعمل مع بقية الاختبارات، وقد شرحنا مكانه بين أنواع الاختبارات في [اختبارات الأمان](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/#security)، ولاحظ أن OWASP Top 10 لعام 2025 تقول في [صفحة A01](https://top10.owasp.org/2025/A01%5F2025-Broken%5FAccess%5FControl?ref=arabroot.io) إن على المطورين وفريق الجودة إضافة اختبارات وظيفية للتحكم بالوصول في اختبارات الوحدة والتكامل.

### اختيار متطلبات الميزة Feature من ملف CSV الرسمي لمعيار ASVS

قراءة 345 متطلباً لكل ميزة غير عملية، والأسهل أن تأخذ ملف CSV الرسمي وتستخرج منه أقسام الميزة حتى المستوى الذي اخترته. وأولاً نقوم بتنزيل الملف من صفحة الإصدار:

```bash
mkdir -p ~/lab-asvs && cd ~/lab-asvs
curl -L -o asvs-5.0.0.csv \
  https://github.com/OWASP/ASVS/releases/download/v5.0.0_release/OWASP_Application_Security_Verification_Standard_5.0.0_en.csv
head -1 asvs-5.0.0.csv
```

```text
chapter_id,chapter_name,section_id,section_name,req_id,req_description,L
```

ثم نكتب سكربت Python صغيراً باسم `asvs_pick.py` يأخذ المستوى المستهدف وأرقام الأقسام:

```python
import csv
import sys
from collections import Counter

rows = list(csv.DictReader(open("asvs-5.0.0.csv", encoding="utf-8")))
target = int(sys.argv[1])          # the level we chose for the app, e.g. 2
sections = sys.argv[2:]            # the sections this feature touches, e.g. V8.2 V1.2

print("all requirements:", len(rows), dict(sorted(Counter(r["L"] for r in rows).items())))
print("required for L%d:" % target, sum(int(r["L"]) <= target for r in rows))
for r in rows:
    if r["section_id"] in sections and int(r["L"]) <= target:
        print(f'v5.0.0-{r["req_id"][1:]}  L{r["L"]}  {r["req_description"][:88]}')
```

ونشغله داخل Container من Docker Image الرسمية لبايثون، على أقسام الصلاحيات ومنع الحقن والعشوائية والسجلات والاعتماديات:

```bash
docker run --rm --name lab-asvs-pick -v "$PWD:/lab:ro" -w /lab python:3.12-slim \
  python asvs_pick.py 2 V8.2 V1.2 V11.5 V16.2 V15.2
```

والمخرج سوف يكون كما يلي:

```text
all requirements: 345 {'1': 70, '2': 183, '3': 92}
required for L2: 253
v5.0.0-1.2.1  L1  Verify that output encoding for an HTTP response, HTML document, or XML document is rele
v5.0.0-1.2.2  L1  Verify that when dynamically building URLs, untrusted data is encoded according to its c
v5.0.0-1.2.3  L1  Verify that output encoding or escaping is used when dynamically building JavaScript con
v5.0.0-1.2.4  L1  Verify that data selection or database queries (e.g., SQL, HQL, NoSQL, Cypher) use param
v5.0.0-1.2.5  L1  Verify that the application protects against OS command injection and that operating sys
v5.0.0-1.2.6  L2  Verify that the application protects against LDAP injection vulnerabilities, or that spe
v5.0.0-1.2.7  L2  Verify that the application is protected against XPath injection attacks by using query
v5.0.0-1.2.8  L2  Verify that LaTeX processors are configured securely (such as not using the "--shell-esc
v5.0.0-1.2.9  L2  Verify that the application escapes special characters in regular expressions (typically
v5.0.0-8.2.1  L1  Verify that the application ensures that function-level access is restricted to consumer
v5.0.0-8.2.2  L1  Verify that the application ensures that data-specific access is restricted to consumers
v5.0.0-8.2.3  L2  Verify that the application ensures that field-level access is restricted to consumers w
v5.0.0-11.5.1  L2  Verify that all random numbers and strings which are intended to be non-guessable must b
v5.0.0-15.2.1  L1  Verify that the application only contains components which have not breached the documen
v5.0.0-15.2.2  L2  Verify that the application has implemented defenses against loss of availability due to
v5.0.0-15.2.3  L2  Verify that the production environment only includes functionality that is required for
v5.0.0-16.2.1  L2  Verify that each log entry includes necessary metadata (such as when, where, who, what)
v5.0.0-16.2.2  L2  Verify that time sources for all logging components are synchronized, and that timestamp
v5.0.0-16.2.3  L2  Verify that the application only stores or broadcasts logs to the files and services tha
v5.0.0-16.2.4  L2  Verify that logs can be read and correlated by the log processor that is in use, prefera
v5.0.0-16.2.5  L2  Verify that when logging sensitive data, the application enforces logging based on the d
```

في المخرج أعلاه لاحظ التالي:

- السطر الأول يؤكد الأعداد من الملف نفسه: 345 متطلباً، منها 70 في المستوى الأول و183 في الثاني و92 في الثالث، وتطبيق المستوى الثاني يلزمه 253 متطلباً.
- بعض المتطلبات لا تخص الميزة، مثل LDAP وXPath وLaTeX في القسم 1.2، وهنا يأتي دور الإنسان: تختار من القائمة ما ينطبق وتكتب سبب استبعاد الباقي، ولا تلصق القائمة كلها في التذكرة.
- الخمسة التي اخترناها للتذكرة INV-142 هي 1.2.4 و8.2.2 و11.5.1 و15.2.1 و16.2.5، وسوف ترى كل واحد منها يتحول إلى تعليق مراجعة واختبار في المثال العملي.

## ASVS بجانب ISO 27001 وضوابط NCA ECC والمراجع الأخرى References

كثير من الفرق تسأل عن ASVS لأن عليها التزاماً بإطار حوكمة، والإجابة المختصرة أن ASVS ليس إطار امتثال، ولكنه مصدر جيد للمتطلبات التقنية التي تطلبها هذه الأطر من التطبيقات. وما يلي قراءتنا للنصوص الرسمية وليس استشارة قانونية، والحكم على الامتثال يعود إلى الجهة المنظمة والمدقق:

- **الضوابط الأساسية للأمن السيبراني NCA ECC:** في [الإصدار ECC-2:2024](https://cdn.nca.gov.sa/api/files/public/upload/86e09090-44e4-481f-bc28-355673607654%5FECC--2024-EN.pdf?ref=arabroot.io) من [الضوابط الأساسية](https://nca.gov.sa/ar/regulatory-documents/controls-list/ecc/?ref=arabroot.io)، يطلب الضابط 1-6-3 لمشاريع تطوير البرمجيات والتطبيقات استخدام معايير البرمجة الآمنة، واستخدام مصادر موثوقة ومرخصة لأدوات التطوير والمكتبات، وإجراء اختبار التزام البرمجيات بمتطلبات الأمن السيبراني للجهة، والتكامل الآمن بين التطبيقات. ويطلب المكون 2-15 الخاص بحماية تطبيقات الويب الخارجية أن تحدد متطلبات الحماية وتوثق وتعتمد (2-15-1) ثم تطبق (2-15-2) وتراجع دورياً (2-15-4)، ومن الحد الأدنى فيها جدار حماية تطبيقات الويب WAF والمعمارية متعددة الطبقات Multi-tier وHTTPS وتحديد عوامل المصادقة بحسب أثر تجاوزها. ومتطلبات ASVS بمستوى تختاره وتوثقه تصلح أساساً لهذه المتطلبات الموثقة، وتقرير التحقق منها يصلح دليلاً على «اختبار الالتزام».
- **المعيار ISO/IEC 27001:2022:** في ملحق الضوابط Annex A من [المعيار](https://www.iso.org/standard/27001?ref=arabroot.io) مجموعة ضوابط للتطوير الآمن، منها دورة حياة التطوير الآمن (8.25) ومتطلبات أمان التطبيقات (8.26) والبرمجة الآمنة (8.28) والاختبار الأمني أثناء التطوير والقبول (8.29). والمعيار يطلب أن تحدد المتطلبات وتطبق، ولا يحدد ما هي، وهنا يملأ ASVS الفراغ بقائمة محددة يمكن التحقق منها.
- **إطار التطوير الآمن NIST SSDF:** المنشور [SP 800-218](https://csrc.nist.gov/pubs/sp/800/218/final?ref=arabroot.io) (الإصدار 1.1، فبراير 2022) يصف ممارسات التطوير الآمن على مستوى المؤسسة، ومنها الممارسة PW.7 التي عنوانها «Review and/or Analyze Human-Readable Code to Identify Vulnerabilities and Verify Compliance with Security Requirements»، أي مراجعة الكود وتحليله للبحث عن الثغرات وللتحقق من الالتزام بمتطلبات الأمان، وهي بالضبط موضوع هذا المقال، وقد نشر NIST [مسودة الإصدار 1.2](https://csrc.nist.gov/pubs/sp/800/218/r1/ipd?ref=arabroot.io) في 17 ديسمبر 2025 وأغلق باب التعليقات عليها في 30 يناير 2026\. وقد شرحنا مكانه في [دليل DevSecOps](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/).
- **تطبيقات الجوال OWASP MASVS:** ASVS يغطي تطبيقات الويب والخدمات، أما تطبيق الجوال نفسه فله [معيار MASVS](https://mas.owasp.org/MASVS/?ref=arabroot.io)، وآخر إصدار له [v2.1.0](https://github.com/OWASP/masvs/releases/tag/v2.1.0?ref=arabroot.io) في يناير 2024، وفيه مجموعات ضوابط مثل MASVS-STORAGE وMASVS-CRYPTO وMASVS-AUTH وMASVS-NETWORK وMASVS-PRIVACY، ولاحظ أن المستويات في عائلة MAS أصبحت ملفات تعريف MAS Profiles مثل MAS-L1 وMAS-L2 وMAS-R. والخادم الذي يخدم تطبيق الجوال يبقى تحت ASVS.
- **نضج البرنامج كله OWASP SAMM:** إذا كان سؤالك «ما مدى نضج طريقة عملنا؟» وليس «هل هذا التطبيق آمن؟» فالمرجع هو [نموذج SAMM](https://owaspsamm.org/model/?ref=arabroot.io)، وفيه خمس وظائف Business Functions هي Governance وDesign وImplementation وVerification وOperations، ولكل واحدة ثلاث ممارسات، ومنها [Security Requirements](https://owaspsamm.org/model/design/security-requirements/?ref=arabroot.io) تحت Design وRequirements-driven Testing تحت Verification، وهما بالضبط ما يصفه هذا المقال.

## مراجعة الكود الأمنية Security Code Review: ما الذي يراه الإنسان ولا تراه الأداة؟

بعد أن تكتب المتطلبات، يبقى سؤال: من يتحقق أن الكود الذي دمج اليوم يطبقها؟ والحلول الأولى المعتادة هي:

1. **مراجعة الأسلوب فقط:** المراجع ينظر إلى الأسماء والتنسيق والاختبارات ثم يكتب «LGTM»، وهذا مفيد للجودة ولكنه لا يسأل أي سؤال أمني.
2. **الأدوات وحدها:** نضيف SAST وفحص الاعتماديات SCA في CI ونعتبر أن كل ما لم تعترض عليه الأداة آمن، وسوف ترى في المثال العملي أن نصف المشكلات لم تظهر لأي أداة.
3. **فريق الأمن يراجع كل طلب دمج:** لا يوجد فريق أمن في العالم يكفي لهذا، فيتحول إلى بوابة بطيئة يتجاوزها الجميع، كما شرحنا في [فريق الأمن كبوابة أخيرة](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#security-team-gate).

والحل الأنسب أن يقوم كل مراجع بمرور أمني قصير ومحدد على كل طلب دمج، بقائمة مكتوبة، وأن تذهب الملفات الحساسة تلقائياً إلى من يعرفها. وسبب أن الإنسان لا غنى عنه تلخصه OWASP في [Secure Code Review Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secure%5FCode%5FReview%5FCheat%5FSheet.html?ref=arabroot.io) حين تصف المراجعة اليدوية بأنها تكمل أدوات SAST وDAST بالتركيز على ما يحتاج تحليلاً بشرياً، وتذكر منه أخطاء منطق العمل Business Logic Flaws، والثغرات التي تعتمد على قواعد خاصة بالتطبيق، ومنطق الصلاحيات Authorization Logic المعقد، وحالات التسابق Race Conditions، وسوء استخدام التشفير Cryptographic Misuse، وأخطاء التصميم. والقاسم المشترك بين هذه كلها أنها تحتاج أن تعرف ما الذي يجب أن يحدث، والأداة لا تعرف إلا ما يحدث.

وأما [OWASP Code Review Guide](https://owasp.org/www-project-code-review-guide/?ref=arabroot.io) فهو كتاب مفيد في «لماذا وكيف» نراجع، ولكن انتبه لعمره: النسخة الحالية في صفحة المشروع هي الإصدار الثاني بصيغة PDF من يوليو 2017، والمشروع مصنف Lab، وأغلب فصول الثغرات فيه مبنية على قائمة OWASP Top 10 لعام 2013\. لذلك استخدمه للمنهجية، وخذ القوائم التفصيلية من Secure Code Review Cheat Sheet ومن ASVS 5.0 لأنهما أحدث.

### كيف تراجع طلب دمج Pull Request أمنياً في عشرين دقيقة؟

الطريقة التي نقترحها ثمانية أسئلة بترتيب ثابت، والترتيب مهم لأن كل سؤال يضيق المساحة للسؤال الذي بعده، فلا تقرأ الكود من أول سطر إلى آخره، وإنما تبدأ من المكان الذي يدخل منه المهاجم. والصورة التالية تبين الترتيب:

![ثماني بطاقات مرقمة في صفين: 1 ماذا تغير في هذا الطلب files endpoints data مع وثيقة، 2 أين يعبر الطلب حد ثقة trust boundaries in the diff مع سيرفر البوابة، 3 من أين تدخل البيانات inputs body query headers files مع متصفح، 4 أين تنتهي هذه البيانات sinks SQL shell HTML logs HTTP مع قاعدة بيانات، 5 من يحق له وعلى أي بيانات authN + authZ on every new endpoint مع قفل، 6 الأسرار والتشفير والعشوائية مع مفتاح، 7 ماذا يدخل السجل ورسالة الخطأ logging error handling مع شاشة طرفية، 8 حزمة جديدة أو تغيير في الجداول dependencies migrations مع مكدس حزم](https://arabroot.io/content/images/2026/10/asvs-review-03-review-path.webp)

ابدأ من حدود الثقة والمدخلات، ثم تتبع كل مدخل حتى المكان الذي ينتهي فيه، ثم الأسئلة التي لا تراها الأدوات

1. **ماذا تغير؟ (دقيقتان):** اقرأ وصف الطلب وقائمة الملفات قبل الكود، وحدد المسارات Endpoints الجديدة، والجداول الجديدة، والاعتماديات الجديدة، وأي ملف في مجلد حساس مثل المصادقة أو التشفير أو خط CI. طلب الدمج الصغير يراجع أفضل، وقد شرحنا السبب في [طلبات دمج صغيرة ومراجعة سريعة](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D9%81%D8%B1%D8%B9-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-trunk-based/#small-prs).
2. **حدود الثقة Trust Boundaries في الفرق Diff:** أين يستقبل الكود الجديد شيئاً من خارج سيطرته؟ طلب HTTP، أو Webhook من مزود، أو ملف مرفوع، أو رسالة من طابور Queue، أو استجابة من خدمة خارجية. وإذا كان عندك نموذج تهديدات للميزة فهذه هي الأسهم الحمراء فيه.
3. **المدخلات Inputs:** لكل حد ثقة، ما الحقول التي تدخل؟ الجسم Body والاستعلام Query والترويسات Headers والمسار Path والملفات، وهل يوجد تحقق Validation بقائمة مسموح Allow List كما يطلب المتطلب 2.2.1؟
4. **الأماكن الخطرة Sinks:** تتبع كل مدخل حتى المكان الذي ينتهي فيه: استعلام SQL، أو أمر Shell، أو قالب HTML، أو مسار ملف، أو طلب HTTP إلى عنوان يحدده المستخدم، أو سطر في السجل. وإذا وصل المدخل إلى أحدها دون معاملات Parameters أو ترميز Encoding فهذه ملاحظة، وهذه هي فكرة تتبع البيانات الملوثة Taint Analysis نفسها التي شرحناها في [دليل SAST](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/#taint)، ولكنك تقوم بها بعقلك على الفرق فقط.
5. **المصادقة والصلاحيات AuthN/AuthZ:** لكل مسار جديد سؤالان منفصلان: من يحق له أن يستدعي هذه الوظيفة؟ (المتطلب 8.2.1) وعلى أي بيانات؟ (المتطلب 8.2.2). والطريقة العملية أن تتخيل مستخدماً ثانياً من مستأجر آخر يرسل الطلب نفسه برقم مختلف، وأن تتأكد أن الفحص في الخادم وليس في الواجهة (8.3.1). وقد شرحنا نماذج الصلاحيات وأين يكون الفحص في [دليل الصلاحيات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B5%D9%84%D8%A7%D8%AD%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%83%D8%B2%D9%8A%D8%A9-authorization-rbac-%D9%88-rebac/#where-checks).
6. **الأسرار والتشفير والعشوائية:** هل دخل الكود مفتاح أو كلمة مرور؟ (أداة فحص الأسرار تساعد هنا، كما في [دليل اكتشاف الأسرار](https://arabroot.io/articles/%D8%A7%D9%83%D8%AA%D8%B4%D8%A7%D9%81-%D8%A7%D9%84%D8%A7%D8%B3%D8%B1%D8%A7%D8%B1-%D8%A7%D9%84%D9%85%D8%B3%D8%B1%D8%A8%D8%A9-%D9%85%D8%B9-gitleaks-%D9%88-trufflehog/)). وهل يولد الكود توكن أو رمزاً أو معرفاً لا يجب أن يخمن؟ إذا كان كذلك فيجب أن يأتي من مولد آمن تشفيرياً (11.5.1). وهل استخدم الكود خوارزمية أو وضع تشفير بنفسه؟ وتفاصيل ذلك في [التشفير للمطورين](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B4%D9%81%D9%8A%D8%B1-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D8%A7%D9%84%D9%87%D8%A7%D8%B4-%D9%88%D9%83%D9%84%D9%85%D8%A7%D8%AA-%D8%A7%D9%84%D9%85%D8%B1%D9%88%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%A7%D8%AA%D9%8A%D8%AD/).
7. **السجلات Logs ورسائل الخطأ:** هل يكتب الكود الجديد في السجل توكناً أو كلمة مرور أو بيانات شخصية (16.2.5)؟ وهل ترجع رسالة الخطأ للمستخدم تتبع الاستدعاءات Stack Trace أو نص الاستعلام (16.5.1)؟ وما لا يدخل السجل أبداً شرحناه في [دليل السجلات](https://arabroot.io/articles/%D8%A7%D9%84%D8%B3%D8%AC%D9%84%D8%A7%D8%AA-logging-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%88%D9%85%D8%AF%D9%8A%D8%B1%D9%8A-%D8%A7%D9%84%D8%B3%D9%8A%D8%B1%D9%81%D8%B1%D8%A7%D8%AA/#never-log)، وما يجب أن يدخل سجل التدقيق في [دليل سجل التدقيق](https://arabroot.io/articles/%D8%B3%D8%AC%D9%84-%D8%A7%D9%84%D8%AA%D8%AF%D9%82%D9%8A%D9%82-audit-trail-%D9%85%D9%86-%D9%81%D8%B9%D9%84-%D9%85%D8%A7%D8%B0%D8%A7-%D9%88%D9%85%D8%AA%D9%89/).
8. **الاعتماديات Dependencies والتهجير Migrations:** كل حزمة جديدة تسأل عنها: لماذا نحتاجها؟ وما ترخيصها؟ وهل عليها ثغرات معروفة؟ وهل تحدث ملف القفل Lockfile؟ وهذه أسئلة [أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/). وكل تهجير في قاعدة البيانات تسأل: هل يكشف عموداً حساساً لدور لا يجب أن يراه؟ وهل يحذف بيانات لا ترجع؟

وهل تكفي عشرون دقيقة؟ لطلب دمج صغير من مئتي سطر نعم في الغالب، لأنك لا تقرأ كل سطر بالعمق نفسه، وإنما تقرأ بعمق ما يقع على الطريق من المدخل إلى المكان الخطر. وإذا احتاج الطلب أكثر من ذلك فهذه إشارة إلى أنه كبير ويجب تقسيمه، أو أنه يلمس شيئاً حساساً ويحتاج صاحب ذلك الجزء.

### نتائج SAST إشارات Hints وليست أحكاماً

عندما تعمل أداة SAST في CI قبل المراجعة، فاستخدم نتائجها لتعرف أين تنظر، ولا تستخدمها لتعرف أين لا تنظر. والسبب أن للأداة نوعين من الخطأ: الإنذار الخاطئ False Positive الذي يضيع وقتك، والإنذار المفقود False Negative الذي يعطيك ثقة لا تستحقها، وسوف ترى الاثنين في المثال العملي على سطر واحد. لذلك القاعدة في المراجعة: كل نتيجة تقرأ وتأخذ قراراً (إصلاح أو استثناء مكتوب السبب)، ولا تقم بتخطي سؤال الصلاحيات ومنطق العمل لأن الأداة سكتت عنه، والسبب أن الأداة لا تفحص هذين أصلاً. وقد شرحنا طريقة التعامل مع الإنذار الخاطئ في [دليل SAST](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/#lab-false-positive)، وترتيب النتائج في [دليل DevSecOps](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#triage).

### ملف CODEOWNERS: الملف الحساس يذهب إلى من يعرفه

بعض الملفات يجب ألا يدمج تغييرها دون موافقة شخص معين: كود المصادقة، والتشفير، والتهجير، وملفات خط CI، وملفات الاعتماديات. وبدلاً من أن تتذكر ذلك في كل مرة، تكتبه في ملف CODEOWNERS. وبحسب [توثيق Gitea](https://docs.gitea.com/usage/code-owners?ref=arabroot.io) يبحث Gitea عن الملف في `./CODEOWNERS` ثم `./docs/CODEOWNERS` ثم `./.gitea/CODEOWNERS` ويتوقف عند أول ملف يجده، وكل سطر فيه تعبير نمطي Regex بصيغة Go ثم مستخدم أو فريق أو أكثر، ويطلب Gitea تلقائياً مراجعة المالكين لكل طلب دمج يغير ملفاً يطابق قاعدة. والمثال التالي ملف `.gitea/CODEOWNERS` لتطبيق الفواتير:

```text
# security-sensitive paths: the security team must approve
app/auth/.* @acme/security
app/crypto/.* @acme/security
migrations/.* @acme/security @acme/dba
requirements.*\\.(txt|lock) @acme/security
\\.gitea/workflows/.* @acme/platform
```

في الملف أعلاه لاحظ التالي:

- النقطة في التعبير النمطي تكتب `\\.` لأن توثيق Gitea يطلب مضاعفة الشرطة المائلة العكسية للأحرف الخاصة داخل الملف.
- الملف وحده يطلب المراجعة ولا يمنع الدمج، والمنع يأتي من قاعدة حماية الفرع Branch Protection في Gitea عندما تفعل خيار فرض موافقة مالك الكود، فلا يدمج الطلب حتى يوافق مالك واحد على الأقل لكل قاعدة تنطبق. وقد شرحنا حماية الفرع في [دليل Trunk-Based](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B7%D9%88%D9%8A%D8%B1-%D8%B9%D9%84%D9%89-%D8%A7%D9%84%D9%81%D8%B1%D8%B9-%D8%A7%D9%84%D8%B1%D8%A6%D9%8A%D8%B3%D9%8A-trunk-based/#branch-protection).
- في GitHub الاسم نفسه ولكن المواقع `.github/` ثم المجلد الرئيسي للمستودع ثم `docs/` بحسب [توثيق GitHub](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners?ref=arabroot.io)، والصيغة أنماط تشبه `.gitignore` وليست تعبيرات نمطية، لذلك لا تنسخ الملف بين المنصتين كما هو.
- لا تكتب في CODEOWNERS أسماء أشخاص لكل مجلد، فالمالك الذي يوافق على كل شيء لا يراجع شيئاً. الملف للأجزاء الحساسة فقط، وأدوار المساهمين والمشرفين في المشاريع شرحناها في [دليل حوكمة المشاريع مفتوحة المصدر](https://arabroot.io/articles/%D8%AD%D9%88%D9%83%D9%85%D8%A9-%D8%A7%D9%84%D9%85%D8%B4%D8%A7%D8%B1%D9%8A%D8%B9-%D9%85%D9%81%D8%AA%D9%88%D8%AD%D8%A9-%D8%A7%D9%84%D9%85%D8%B5%D8%AF%D8%B1-%D9%88%D8%A7%D9%84%D9%85%D8%B3%D8%A7%D9%87%D9%85%D8%A9-%D9%81%D9%8A%D9%87%D8%A7/#roles).

ومن هو `@acme/security` في فريق صغير ليس فيه فريق أمن؟ هنا يأتي دور سفير الأمن Security Champion: مطور في كل فريق عنده اهتمام بالأمن ووقت مخصص له، يكون المالك الأول لهذه المسارات ويحول ما يحتاج خبرة أعمق إلى من يملكها، وقد شرحنا الفكرة في [سفير الأمن في كل فريق](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/#champions)، ولها دليل من OWASP في [Security Champions Guide](https://securitychampions.owasp.org/?ref=arabroot.io).

## قائمة المراجعة الأمنية Security Checklist لكل طلب دمج Pull Request، جاهزة للنسخ

القائمة التالية قصيرة عن قصد، وكل بند فيها يحمل رقم المتطلب في ASVS 5.0.0 حتى يعرف من يقرأها لماذا هو موجود. والفكرة أن يملأها كاتب الطلب قبل أن يطلب المراجعة، ثم يتحقق منها المراجع، فلا تكون عبئاً على شخص واحد:

```markdown
## Security checklist (ASVS 5.0.0)

### Author
- [ ] Every new endpoint checks who may call it (8.2.1) and which records they may see (8.2.2), on the server (8.3.1).
- [ ] Every new input is validated against what we expect (2.2.1).
- [ ] Every query, command and HTML output is parameterized or encoded (1.2.x).
- [ ] No secret in code, config, tests or fixtures; new secrets come from the secret store (13.3.1).
- [ ] Tokens, codes and ids that must not be guessed come from a CSPRNG (11.5.1).
- [ ] No password, token, key or personal data is written to logs (16.2.5).
- [ ] Errors return a generic message, no stack trace or query (16.5.1).
- [ ] New or updated dependency: reason, license, advisories checked, lock file updated (15.2.1).
- [ ] Migration: no sensitive column exposed to a new role, and a way back exists.
- [ ] Security tests added for the ASVS ids in the ticket (allowed AND denied cases).
- [ ] Threat model updated if a trust boundary, data store or external service changed.

### Reviewer
- [ ] I traced each new input to the place where it ends.
- [ ] I tried every new endpoint in my head as a user of another tenant.
- [ ] I read every SAST/SCA finding of this PR: fixed, or ignored with a written reason.
- [ ] Sensitive paths were reviewed by their CODEOWNERS.
```

ولا تحول القائمة إلى خمسين بنداً، والسبب أن القائمة الطويلة يضع الناس علامة على كل بنودها دون قراءة. وإذا تكرر في المراجعات خطأ ليس في القائمة، فأضف له بنداً واحذف بنداً لم يجد شيئاً منذ أشهر.

### ملف قالب طلب الدمج PR Template في Gitea وGitHub

أفضل مكان للقائمة هو وصف طلب الدمج نفسه، لأنه أول ما يراه الكاتب والمراجع. وبحسب [توثيق Gitea](https://docs.gitea.com/usage/issue-pull-request-templates?ref=arabroot.io) يقبل Gitea ملف القالب بالأسماء `PULL_REQUEST_TEMPLATE.md` أو `pull_request_template.md` (أو بصيغة `.yaml` و`.yml`) في المجلد الرئيسي للمستودع أو في المجلد `.gitea/` أو في المجلد `.github/`، ويقبل في رأس ملف Markdown حقولاً مثل `name` و`about` و`title` و`labels`. وأما GitHub فيقبل بحسب [توثيقه](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository?ref=arabroot.io) الملف `pull_request_template.md` في المجلد الرئيسي أو في `docs/` أو في `.github/`، ويسمح بعدة قوالب داخل مجلد `PULL_REQUEST_TEMPLATE/`. وهذا ملف `.gitea/PULL_REQUEST_TEMPLATE.md` كاملاً:

```markdown
## What does this PR change?
<!-- One or two sentences. Link the ticket, e.g. INV-142. -->

## Security requirements in scope
<!-- ASVS ids from the ticket, e.g. v5.0.0-8.2.2, v5.0.0-11.5.1. Write "none" and why if none apply. -->

## How was it tested?
<!-- Name the tests, including the denied cases (other tenant, bad input). -->

## Security checklist (ASVS 5.0.0)

### Author
- [ ] Every new endpoint checks who may call it (8.2.1) and which records they may see (8.2.2), on the server (8.3.1).
- [ ] Every new input is validated against what we expect (2.2.1).
- [ ] Every query, command and HTML output is parameterized or encoded (1.2.x).
- [ ] No secret in code, config, tests or fixtures; new secrets come from the secret store (13.3.1).
- [ ] Tokens, codes and ids that must not be guessed come from a CSPRNG (11.5.1).
- [ ] No password, token, key or personal data is written to logs (16.2.5).
- [ ] Errors return a generic message, no stack trace or query (16.5.1).
- [ ] New or updated dependency: reason, license, advisories checked, lock file updated (15.2.1).
- [ ] Migration: no sensitive column exposed to a new role, and a way back exists.
- [ ] Security tests added for the ASVS ids in the ticket (allowed AND denied cases).
- [ ] Threat model updated if a trust boundary, data store or external service changed.

### Reviewer
- [ ] I traced each new input to the place where it ends.
- [ ] I tried every new endpoint in my head as a user of another tenant.
- [ ] I read every SAST/SCA finding of this PR: fixed, or ignored with a written reason.
- [ ] Sensitive paths were reviewed by their CODEOWNERS.
```

والصورة التالية تبين أين تقع القائمة بين فتح طلب الدمج ودمجه، ومع أي أدوات تعمل:

![خمس بطاقات من اليمين إلى اليسار فوق خط main أخضر: 1 Short branch فرع قصير وطلب دمج صغير feature/password-reset مع pre-commit gitleaks، 2 PR template القائمة تظهر في وصف الطلب .gitea/PULL_REQUEST_TEMPLATE.md والكاتب يضع العلامات، 3 CI checks الأدوات تعمل قبل البشر secrets SAST SCA tests والنتائج إشارات للمراجع، 4 Review المراجع يستخدم القائمة نفسها CODEOWNERS يطلب @security لمسارات auth وcrypto وmigrations، 5 Protected merge لا دمج دون موافقة approvals + status checks وموافقة مالك الكود، وشريط برتقالي يربط الخطوة 2 بالخطوة 4 مكتوب عليه قائمة واحدة يملؤها الكاتب في الخطوة 2 ويتحقق منها المراجع في الخطوة 4](https://arabroot.io/content/images/2026/10/asvs-review-05-pr-flow.webp)

القائمة لا تعمل وحدها: القالب يذكر الكاتب، وCI يعطي الإشارات، وCODEOWNERS يحضر الشخص المناسب، وحماية الفرع تمنع الدمج دونه

## مثال عملي Lab: نراجع طلب دمج Pull Request فيه خمس مشكلات

الآن سوف نطبق كل ما سبق على طلب دمج حقيقي الشكل كتبناه لهذا المقال: تطبيق فواتير صغير بلغة Python مع Flask وقاعدة SQLite، وطلب دمج ينفذ التذكرة INV-142 كما قد يكتبها مطور مستعجل، وفيه خمس مشكلات مقصودة. وسوف نكتب تعليقات المراجعة، ثم نشغل Semgrep وOSV-Scanner والاختبارات الأمنية لنرى ما يجده كل منها، ثم نصلح. كل التشغيل داخل Containers تحذف نفسها بعد الانتهاء (`--rm`)، دون أي منفذ مفتوح على الجهاز.

### التطبيق Application قبل طلب الدمج

نجهز ثلاثة مجلدات: `main` للكود قبل الطلب، و`pr` للكود في الطلب، و`fix` للكود بعد الإصلاح، وفي كل منها الملف `schema.sql` نفسه، وفيه مستخدمان من مستأجرين مختلفين وفاتورة لكل منهما:

```sql
CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE, tenant_id INTEGER);
CREATE TABLE invoices (id INTEGER PRIMARY KEY, number TEXT, total REAL, tenant_id INTEGER);
CREATE TABLE reset_tokens (user_id INTEGER, token TEXT, expires_at TEXT);
INSERT INTO users VALUES (1, 'a@tenant-a.example', 1), (2, 'b@tenant-b.example', 2);
INSERT INTO invoices VALUES (10, 'A-0001', 1200.0, 1), (20, 'B-0001', 98000.0, 2);
```

والملف `main/app.py` فيه مسار واحد يعرض فواتير مستأجر المستخدم، والملف `main/requirements.txt` فيه السطر `flask==3.1.3` فقط:

```python
import logging
import os
import sqlite3
from functools import wraps

from flask import Flask, abort, g, jsonify, session

app = Flask(__name__)
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
app.config["DB"] = "app.db"
log = logging.getLogger("invoices")

def db():
    if "db" not in g:
        g.db = sqlite3.connect(app.config["DB"])
        g.db.row_factory = sqlite3.Row
    return g.db

def current_user():
    uid = session.get("user_id")
    if uid is None:
        return None
    return db().execute("SELECT id, email, tenant_id FROM users WHERE id = ?", (uid,)).fetchone()

def login_required(view):
    @wraps(view)
    def wrapper(*args, **kwargs):
        if current_user() is None:
            abort(401)
        return view(*args, **kwargs)
    return wrapper

@app.get("/api/invoices")
@login_required
def list_invoices():
    rows = db().execute(
        "SELECT id, number, total FROM invoices WHERE tenant_id = ?",
        (current_user()["tenant_id"],),
    ).fetchall()
    return jsonify([dict(r) for r in rows])
```

لاحظ أن المسار الموجود يفلتر بالمستأجر `tenant_id`، فالفريق يعرف القاعدة، وهذا ما يجعل المشكلة الأولى في الطلب سهلة الفوات: المراجع يرى `@login_required` فيفترض أن الحماية موجودة.

### طلب الدمج كما وصل، والفرق Diff كاملاً

هذا هو الفرق Diff الذي وصل للمراجعة، كما يخرجه `git diff`:

```diff
diff --git a/app.py b/app.py
index 1b8c3d8..84e2623 100644
--- a/app.py
+++ b/app.py
@@ -1,14 +1,18 @@
 import logging
 import os
+import random
 import sqlite3
+import string
 from functools import wraps

-from flask import Flask, abort, g, jsonify, session
+import requests
+from flask import Flask, abort, g, jsonify, request, session

 app = Flask(__name__)
 app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]
 app.config["DB"] = "app.db"
 log = logging.getLogger("invoices")
+MAIL_API = "https://mail.internal.example/send"

 def db():
@@ -42,3 +46,30 @@ def list_invoices():
         (current_user()["tenant_id"],),
     ).fetchall()
     return jsonify([dict(r) for r in rows])
+
+
+@app.get("/api/invoices/<int:invoice_id>")
+@login_required
+def get_invoice(invoice_id):
+    row = db().execute(
+        "SELECT id, number, total FROM invoices WHERE id = ?", (invoice_id,)
+    ).fetchone()
+    if row is None:
+        abort(404)
+    return jsonify(dict(row))
+
+
+@app.post("/api/password-reset")
+def request_reset():
+    email = request.get_json().get("email", "")
+    user = db().execute(f"SELECT id, email FROM users WHERE email = '{email}'").fetchone()
+    if user:
+        token = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
+        db().execute(
+            "INSERT INTO reset_tokens (user_id, token, expires_at) VALUES (?, ?, datetime('now', '+30 minutes'))",
+            (user["id"], token),
+        )
+        db().commit()
+        log.info("reset token %s issued for %s", token, email)
+        requests.post(MAIL_API, json={"to": email, "link": f"https://app.example.com/reset?t={token}"}, timeout=5)
+    return jsonify({"status": "ok"})
diff --git a/requirements.txt b/requirements.txt
index e6365da..a96a1d3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,2 @@
 flask==3.1.3
+requests==2.31.0
```

الكود يعمل، والاختبارات الوظيفية التي كتبها المطور تنجح، والطلب قصير، وهذا بالضبط نوع الطلب الذي يدمج بكلمة «LGTM».

### تعليقات المراجعة Review Comments بالأسئلة الثمانية

الآن نمر على الطلب بالأسئلة الثمانية بالترتيب. السؤال الأول يعطينا مسارين جديدين واعتمادية جديدة، والسؤال الثاني يقول إن كليهما يستقبل طلب HTTP من الإنترنت، ومسار الاستعادة لا يحتاج تسجيل دخول أصلاً، أي أن أي شخص في العالم يستطيع أن يستدعيه. وبعد تتبع المدخلات إلى الأماكن الخطرة والمرور على بقية الأسئلة، كتبنا التعليقات التالية على الطلب:

1. **`app.py:51-59` (يمنع الدمج، ASVS v5.0.0-8.2.2):** المسار `GET /api/invoices/<id>` يتحقق أن المستخدم سجل دخوله، ولكنه لا يتحقق أن الفاتورة لمستأجره، فالمستخدم 1 من المستأجر 1 يستطيع أن يقرأ الفاتورة 20 للمستأجر 2 بتغيير الرقم في الرابط، وهذه ثغرة IDOR بالضبط. أضف `AND tenant_id = ?` كما في `list_invoices`، وارجع 404 في الحالتين حتى لا يعرف المهاجم أن الرقم موجود، وأضف اختباراً بمستخدم من مستأجر آخر.
2. **`app.py:65` (يمنع الدمج، ASVS v5.0.0-1.2.4):** البريد يدخل نص الاستعلام بصيغة f-string، فالقيمة `evil@attacker.example' OR '1'='1` تعيد أول مستخدم في الجدول، فيصدر توكن استعادة لحسابه ويرسل الرابط إلى العنوان الذي كتبه المهاجم لأن السطر 74 يرسل إلى `email` المدخل وليس إلى بريد المستخدم في قاعدة البيانات. استخدم استعلاماً بمعاملات `?`، وأرسل إلى `user["email"]`.
3. **`app.py:67` (يمنع الدمج، ASVS v5.0.0-11.5.1):** الوحدة `random` لا تصلح لتوكن الاستعادة، والتوثيق الرسمي لبايثون يقول في [صفحة random](https://docs.python.org/3/library/random.html?ref=arabroot.io) إن مولداتها «should not be used for security purposes» ويحيل إلى الوحدة `secrets`. استخدم `secrets.token_urlsafe(32)`، واحفظ في الجدول هاش التوكن وليس التوكن نفسه، كما تطلب OWASP في [Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot%5FPassword%5FCheat%5FSheet.html?ref=arabroot.io) أن تولد التوكنات بمولد آمن، وأن تخزن بطريقة آمنة، وأن تلغى بعد الاستخدام.
4. **`app.py:73` (يمنع الدمج، ASVS v5.0.0-16.2.5):** السطر يكتب التوكن والبريد في السجل، وكل من يقرأ السجلات (فريق الدعم، أو نظام جمع السجلات، أو أي شخص وصل إليه) يستطيع أن يغير كلمة مرور أي مستخدم خلال نصف ساعة. سجل الحدث برقم المستخدم فقط.
5. **`requirements.txt:2` (يمنع الدمج، ASVS v5.0.0-15.2.1):** لماذا الإصدار `requests==2.31.0` بالتحديد؟ عليه ثلاث ثغرات منشورة كما سوف يظهر في فحص الاعتماديات، واستخدم آخر إصدار وأضف ملف قفل بالإصدارات التي تثبت فعلاً.

ومعها ملاحظتان ليستا مانعتين: مسار الاستعادة يحتاج حداً لعدد الطلبات Rate Limiting من العنوان الواحد للبريد الواحد كما يطلب المتطلب 2.4.1 الخاص بمنع الأتمتة المسيئة Anti-automation، والرد يجب أن يبقى واحداً سواء وجد البريد أم لم يوجد، وهو كذلك في الطلب فلنبقه كذلك. ولاحظ أن تعليقاً مثل «الكود غير آمن» لا يصلح، فكل تعليق فيه السطر، والمتطلب، والسيناريو الذي يكسر الكود، والإصلاح المقترح.

### ماذا وجدت Semgrep في الفحص الثابت SAST؟

الآن نشغل Semgrep على مجلد الطلب بحزم القواعد نفسها التي استخدمناها في دليل DevSecOps، مع استبعاد ملف الاختبارات:

```bash
docker run --rm --name lab-asvs-semgrep -v "$PWD/pr:/src:ro" -w /src semgrep/semgrep:1.178.0 \
  semgrep scan --config p/python --config p/flask --metrics=off \
  --exclude test_security.py --emacs --quiet
```

والمخرج سوف يكون كما يلي:

```text
app.py:65:25:error(tainted-sql-string):    user = db().execute(f"SELECT id, email FROM users WHERE email = '{email}'").fetchone():Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using an object-relational mapper (ORM) such as SQLAlchemy which will protect your queries.
app.py:73:9:warning(python-logger-credential-disclosure):        log.info("reset token %s issued for %s", token, email):Detected a python logger call with a potential hardcoded secret "reset token %s issued for %s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging  sensitive information.
```

في المخرج أعلاه لاحظ التالي:

- القاعدة `tainted-sql-string` وجدت استعلام SQL المبني بالنص، وهي قاعدة تتبع ملوث Taint تعرف أن `request.get_json()` مدخل من المستخدم وأن `execute` مكان خطر، وهذه النتيجة صحيحة.
- القاعدة `python-logger-credential-disclosure` نبهت على سطر السجل، ولكن اقرأ رسالتها: «potential hardcoded secret "reset token %s issued for %s"»، أي أنها انتبهت لكلمة token داخل نص الرسالة، ولا تعرف أن المتغير `token` سر، وسوف نختبر هذا بعد قليل.
- لا توجد أي نتيجة على المسار الذي لا يفحص المستأجر، لأن الكود صحيح من جهة الأنماط، والقاعدة الناقصة موجودة في التذكرة وليست في الكود.
- لا توجد أي نتيجة على `random.choice`، فلا توجد في هذه الحزم قاعدة تعرف أن القيمة الناتجة توكن استعادة.

وحتى لا نظلم الأداة، شغلنا أيضاً الحزمة `p/security-audit` مع الحزم السابقة، فلم يتغير شيء في هاتين النتيجتين: لا شيء على الصلاحيات ولا على `random`.

### نكسر الأداة عمداً: كلمة واحدة تغير النتيجة Finding

هل وجدت Semgrep مشكلة السجل فعلاً؟ لنغير نص الرسالة فقط دون أن نغير ما يكتب في السجل، فنضع في نسخة من الملف داخل مجلد `variant` هذا السطر بدلاً من السطر 73:

```python
        log.info("reset requested by %s, code %s", email, token)
```

ثم نشغل الأمر نفسه على المجلد `variant`، والمخرج سوف يكون كما يلي:

```text
app.py:65:25:error(tainted-sql-string):    user = db().execute(f"SELECT id, email FROM users WHERE email = '{email}'").fetchone():Detected user input used to manually construct a SQL string. This is usually bad practice because manual construction could accidentally result in a SQL injection. An attacker could use a SQL injection to steal or modify contents of the database. Instead, use a parameterized query which is available by default in most database engines. Alternatively, consider using an object-relational mapper (ORM) such as SQLAlchemy which will protect your queries.
```

التوكن ما زال يكتب في السجل كما هو، ولكن الإنذار اختفى لأن كلمة token لم تعد في نص الرسالة، وهذا إنذار مفقود False Negative. والعكس يحدث أيضاً: في الكود بعد الإصلاح الذي سوف تراه بعد قليل، وقبل أن نضيف تعليق الاستثناء، أعطت الأداة على سطر لا يكتب أي سر النتيجة التالية:

```text
app.py:79:9:warning(python-logger-credential-disclosure):        log.info("password reset requested user_id=%s", user["id"]):Detected a python logger call with a potential hardcoded secret "password reset requested user_id=%s" being logged. This may lead to secret credentials being exposed. Make sure that the logger is not logging  sensitive information.
```

وهذا إنذار خاطئ False Positive سببه كلمة password في النص. لذلك القاعدة التي ذكرناها: نتيجة الأداة سؤال يجب أن يجيب عنه إنسان، والسطر الذي سكتت عنه الأداة يراجع بالعين كأنها لم تعمل. وفي الإصلاح عالجنا الإنذار الخاطئ بتعليق `nosemgrep` على القاعدة نفسها مع السبب، كما يشرح [توثيق Semgrep](https://semgrep.dev/docs/ignoring-files-folders-code?ref=arabroot.io)، بدلاً من تعطيل القاعدة كلها.

### ماذا وجد فحص الاعتماديات SCA؟

Semgrep لا يقرأ ملف `requirements.txt` بحثاً عن ثغرات الحزم، فهذا عمل أداة فحص الاعتماديات SCA. نشغل [OSV-Scanner](https://github.com/google/osv-scanner?ref=arabroot.io) على الملف:

```bash
docker run --rm --name lab-asvs-osv -v "$PWD/pr:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
  scan source -L /src/requirements.txt
```

والمخرج سوف يكون كما يلي:

```text
Starting filesystem walk for root: /
Scanned /src/requirements.txt file and found 2 packages
End status: 0 dirs visited, 1 inodes visited, 1 Extract calls, 3.667667ms elapsed, 3.667867ms wall time

Total 2 packages affected by 4 known vulnerabilities (0 Critical, 0 High, 4 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
4 vulnerabilities can be fixed.

+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+
| OSV URL                             | CVSS | ECOSYSTEM | PACKAGE  | VERSION | FIXED VERSION | SOURCE               |
+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+
| https://osv.dev/PYSEC-2026-1872     | 5.3  | PyPI      | requests | 2.31.0  | 2.32.4        | src/requirements.txt |
| https://osv.dev/GHSA-9hjg-9r4m-mvj7 |      |           |          |         |               |                      |
| https://osv.dev/PYSEC-2026-1873     | 5.6  | PyPI      | requests | 2.31.0  | 2.32.0        | src/requirements.txt |
| https://osv.dev/GHSA-9wx4-h78v-vm56 |      |           |          |         |               |                      |
| https://osv.dev/PYSEC-2026-2275     | 5.5  | PyPI      | requests | 2.31.0  | 2.33.0        | src/requirements.txt |
| https://osv.dev/GHSA-gc5v-m9x4-r6x2 |      |           |          |         |               |                      |
| https://osv.dev/PYSEC-2026-215      | 6.9  | PyPI      | idna     | 3.9.0   | 3.15          | src/requirements.txt |
| https://osv.dev/GHSA-65pc-fj4g-8rjx |      |           |          |         |               |                      |
+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+
```

في المخرج أعلاه لاحظ التالي:

- الحزمة `requests` بالإصدار 2.31.0 عليها ثلاث ثغرات منشورة: [GHSA-9wx4-h78v-vm56](https://osv.dev/vulnerability/GHSA-9wx4-h78v-vm56?ref=arabroot.io) (كائن Session لا يتحقق من الشهادة بعد أول طلب بـ `verify=False`)، و[GHSA-9hjg-9r4m-mvj7](https://osv.dev/vulnerability/GHSA-9hjg-9r4m-mvj7?ref=arabroot.io) (تسرب بيانات الدخول من ملف `.netrc` عبر روابط مصنوعة)، و[GHSA-gc5v-m9x4-r6x2](https://osv.dev/vulnerability/GHSA-gc5v-m9x4-r6x2?ref=arabroot.io) (إعادة استخدام ملف مؤقت بطريقة غير آمنة)، وكل منها يظهر مرتين بمعرفين من قاعدتي بيانات مختلفتين.
- السطر الأخير عن الحزمة `idna` بالإصدار 3.9.0 يحتاج انتباهاً، فهذه الحزمة ليست في الملف أصلاً، والأداة استنتجت الاعتماديات غير المباشرة Transitive Dependencies بنفسها واختارت هذا الإصدار.

هل يثبت pip فعلاً الإصدار 3.9.0 من `idna`؟ لنسأله دون أن يثبت شيئاً:

```bash
docker run --rm --name lab-asvs-pip python:3.12-slim sh -c 'pip install -q --no-cache-dir \
  --root-user-action=ignore --dry-run --report - requests==2.31.0 2>/dev/null | python -c \
  "import json,sys; [print(i[\"metadata\"][\"name\"], i[\"metadata\"][\"version\"]) for i in json.load(sys.stdin)[\"install\"]]"'
```

```text
requests 2.31.0
certifi 2026.7.22
charset-normalizer 3.5.2
idna 3.20
urllib3 2.8.0
```

في يوم التجربة كان pip سوف يثبت الإصدار 3.20 من `idna` وليس 3.9.0، فالنتيجة الأخيرة تخص إصداراً لا نستخدمه. والمشكلة هنا في المدخل الذي أعطيناه للأداة وليست في الأداة، فالملف `requirements.txt` لا يقول ما الذي يثبت فعلاً، والحل الصحيح ليس تجاهل النتيجة، وإنما ملف قفل فيه كل الحزم بإصداراتها الدقيقة، ثم فحص هذا الملف، وهو ما سوف نفعله في الإصلاح. وتفاصيل ملفات القفل والهاش في [دليل أمن سلسلة التوريد](https://arabroot.io/articles/%D8%A7%D9%85%D9%86-%D8%B3%D9%84%D8%B3%D9%84%D8%A9-%D8%A7%D9%84%D8%AA%D9%88%D8%B1%D9%8A%D8%AF-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D9%8A%D8%A9-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/#lockfiles).

### الاختبارات الأمنية Security Tests من معايير القبول Acceptance Criteria

الأدوات وجدت مشكلتين ونصفاً، والآن نحول معايير القبول في التذكرة إلى اختبارات. الملف `test_security.py` نفسه في المجلدين `pr` و`fix`:

```python
import logging
import sqlite3

import pytest

import app as appmod

SENT = []

@pytest.fixture
def client(tmp_path, monkeypatch):
    path = tmp_path / "test.db"
    con = sqlite3.connect(path)
    con.executescript(open("schema.sql").read())
    con.close()
    appmod.app.config.update(DB=str(path), TESTING=True)
    SENT.clear()
    monkeypatch.setattr(appmod.requests, "post", lambda url, json, timeout: SENT.append(json))
    with appmod.app.test_client() as c:
        yield c

def login(client, user_id):
    with client.session_transaction() as s:
        s["user_id"] = user_id

def tokens_in_db():
    return sqlite3.connect(appmod.app.config["DB"]).execute("SELECT count(*) FROM reset_tokens").fetchone()[0]

def test_own_invoice_is_visible(client):
    login(client, 1)
    assert client.get("/api/invoices/10").status_code == 200

def test_other_tenant_invoice_is_hidden(client):
    # ASVS v5.0.0-8.2.2: tenant A must not read tenant B's invoice
    login(client, 1)
    assert client.get("/api/invoices/20").status_code == 404

def test_reset_token_is_not_logged(client, caplog):
    # ASVS v5.0.0-16.2.5
    caplog.set_level(logging.INFO, logger="invoices")
    client.post("/api/password-reset", json={"email": "a@tenant-a.example"})
    token = SENT[0]["link"].split("t=")[1]
    assert token not in caplog.text
    assert "a@tenant-a.example" not in caplog.text

def test_reset_with_sql_in_email_issues_no_token(client):
    # ASVS v5.0.0-1.2.4: a quote in the input is data, not SQL
    client.post("/api/password-reset", json={"email": "evil@attacker.example' OR '1'='1"})
    assert tokens_in_db() == 0
    assert SENT == []
```

في الملف أعلاه لاحظ التالي:

- الاختبار الأول يثبت الحالة المسموحة، والثاني يثبت الحالة الممنوعة، وأغلب اختبارات الصلاحيات التي نراها تكتب الأولى فقط، فتنجح مع كود لا يفحص أي شيء.
- الدالة `requests.post` مستبدلة بدالة تحفظ الرسالة في القائمة `SENT`، فلا يخرج أي طلب إلى الشبكة، ونأخذ التوكن من الرابط المرسل لنبحث عنه في السجل.
- لا يوجد اختبار للمشكلة الثالثة (`random`)، والسبب أن التوكن من `random` يبدو من الخارج مثل التوكن من `secrets` تماماً: 32 حرفاً عشوائية الشكل. الفرق في طريقة التوليد، ولا يراه إلا من يقرأ الكود.

نشغل الاختبارات على الطلب داخل Container، مع مفتاح للاختبار فقط:

```bash
docker run --rm --name lab-asvs-tests -e SECRET_KEY=test-only -v "$PWD:/lab" -w /lab python:3.12-slim \
  sh -c 'pip install -q --no-cache-dir --root-user-action=ignore flask==3.1.3 requests==2.34.2 pytest==9.1.1 2>/dev/null;
         cd pr && python -m pytest -q -p no:cacheprovider --tb=line test_security.py'
```

والمخرج سوف يكون كما يلي:

```text
.FFF                                                                     [100%]
=================================== FAILURES ===================================
E   AssertionError: assert 200 == 404
     +  where 200 = <WrapperTestResponse streamed [200 OK]>.status_code
     +    where <WrapperTestResponse streamed [200 OK]> = get('/api/invoices/20')
     +      where get = <FlaskClient <Flask 'app'>>.get
/lab/pr/test_security.py:41: AssertionError: assert 200 == 404
E   AssertionError: assert 'SUKCnWad576...STuH2hIKt8HS' not in 'INFO     in...-a.example\n'

      'SUKCnWad576PsMOXGXcTSTuH2hIKt8HS' is contained here:
        INFO     invoices:app.py:73 reset token SUKCnWad576PsMOXGXcTSTuH2hIKt8HS issued for a@tenant-a.example
      ?                                         ++++++++++++++++++++++++++++++++
------------------------------ Captured log call -------------------------------
INFO     invoices:app.py:73 reset token SUKCnWad576PsMOXGXcTSTuH2hIKt8HS issued for a@tenant-a.example
/lab/pr/test_security.py:49: AssertionError: assert 'SUKCnWad576...STuH2hIKt8HS' not in 'INFO     in...-a.example\n'
E   assert 1 == 0
     +  where 1 = tokens_in_db()
/lab/pr/test_security.py:56: assert 1 == 0
=========================== short test summary info ============================
FAILED test_security.py::test_other_tenant_invoice_is_hidden - AssertionError...
FAILED test_security.py::test_reset_token_is_not_logged - AssertionError: ass...
FAILED test_security.py::test_reset_with_sql_in_email_issues_no_token - asser...
3 failed, 1 passed in 0.41s
```

في المخرج أعلاه لاحظ التالي:

- الطلب `GET /api/invoices/20` من مستخدم المستأجر 1 رجع `200 OK`، أي أن فاتورة المستأجر 2 بمبلغ 98000 خرجت لمستخدم لا يملكها، وهي الملاحظة الأولى في المراجعة.
- التوكن الكامل والبريد ظهرا في السجل الملتقط، كما هما.
- البريد الذي فيه `' OR '1'='1` أصدر توكن استعادة حقيقياً في الجدول (`1 == 0`)، أي أن الحقن نجح.
- هذه الاختبارات لم تكن موجودة عندما وصل الطلب، فهي جاءت من معايير القبول التي كتبت من متطلبات ASVS، ولو كانت التذكرة تقول «مع مراعاة الأمان» فقط لما كتبها أحد.

### الكود بعد الإصلاح Fix

وهذا الفرق بين الطلب والإصلاح، وكل تغيير يحمل رقم المتطلب في تعليق:

```diff
diff --git a/app.py b/app.py
index 84e2623..28c8817 100644
--- a/app.py
+++ b/app.py
@@ -1,8 +1,8 @@
+import hashlib
 import logging
 import os
-import random
+import secrets
 import sqlite3
-import string
 from functools import wraps

 import requests
@@ -51,25 +51,32 @@ def list_invoices():
 @app.get("/api/invoices/<int:invoice_id>")
 @login_required
 def get_invoice(invoice_id):
+    # ASVS v5.0.0-8.2.2: data-specific access, the invoice must belong to the caller's tenant
     row = db().execute(
-        "SELECT id, number, total FROM invoices WHERE id = ?", (invoice_id,)
+        "SELECT id, number, total FROM invoices WHERE id = ? AND tenant_id = ?",
+        (invoice_id, current_user()["tenant_id"]),
     ).fetchone()
     if row is None:
-        abort(404)
+        abort(404)  # same answer for "not found" and "not yours"
     return jsonify(dict(row))

 @app.post("/api/password-reset")
 def request_reset():
     email = request.get_json().get("email", "")
-    user = db().execute(f"SELECT id, email FROM users WHERE email = '{email}'").fetchone()
+    # ASVS v5.0.0-1.2.4: parameterized query
+    user = db().execute("SELECT id, email FROM users WHERE email = ?", (email,)).fetchone()
     if user:
-        token = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
+        # ASVS v5.0.0-11.5.1: CSPRNG, 256 bits; only the hash is stored
+        token = secrets.token_urlsafe(32)
+        token_hash = hashlib.sha256(token.encode()).hexdigest()
         db().execute(
             "INSERT INTO reset_tokens (user_id, token, expires_at) VALUES (?, ?, datetime('now', '+30 minutes'))",
-            (user["id"], token),
+            (user["id"], token_hash),
         )
         db().commit()
-        log.info("reset token %s issued for %s", token, email)
-        requests.post(MAIL_API, json={"to": email, "link": f"https://app.example.com/reset?t={token}"}, timeout=5)
-    return jsonify({"status": "ok"})
+        # ASVS v5.0.0-16.2.5: no token and no email in the log, the user id is enough
+        # nosemgrep: python-logger-credential-disclosure (the message has no secret, reviewed in this PR)
+        log.info("password reset requested user_id=%s", user["id"])
+        requests.post(MAIL_API, json={"to": user["email"], "link": f"https://app.example.com/reset?t={token}"}, timeout=5)
+    return jsonify({"status": "ok"})  # same answer whether the email exists or not
diff --git a/requirements.txt b/requirements.txt
index a96a1d3..4c25c8d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 flask==3.1.3
-requests==2.31.0
+requests==2.34.2
```

في الإصلاح أعلاه لاحظ التالي:

- الدالة `secrets.token_urlsafe(32)` تأخذ 32 بايت عشوائية، أي 256 بت، وتوثيق بايثون يقول في [صفحة secrets](https://docs.python.org/3/library/secrets.html?ref=arabroot.io) إن 32 بايت كافية للاستخدام المعتاد، وهي أكثر من 128 بت التي يطلبها المتطلب 11.5.1.
- الجدول يحفظ هاش SHA-256 للتوكن، فإذا تسربت نسخة من قاعدة البيانات لا يستطيع أحد أن يستخدم التوكنات منها، ولأن التوكن عشوائي بطول 256 بت فلا نحتاج هنا دالة بطيئة مثل التي نستخدمها لكلمات المرور. وعند الاستخدام يحسب الخادم هاش التوكن الوارد ويبحث عنه.
- الرسالة ترسل إلى `user["email"]` من قاعدة البيانات وليس إلى ما كتبه الطالب.
- تعليق `nosemgrep` يحدد القاعدة بالاسم ويذكر السبب، فيراه المراجع التالي ويستطيع أن يعترض عليه.

ثم نكتب ملف القفل بالإصدارات التي يثبتها pip فعلاً:

```bash
docker run --rm --name lab-asvs-pip -v "$PWD/fix:/src" -w /src python:3.12-slim sh -c \
  'pip install -q --no-cache-dir --root-user-action=ignore -r requirements.txt 2>/dev/null;
   pip freeze --exclude pip > requirements.lock; cat requirements.lock'
```

```text
blinker==1.9.0
certifi==2026.7.22
charset-normalizer==3.5.2
click==8.5.0
Flask==3.1.3
idna==3.20
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.4
requests==2.34.2
urllib3==2.8.0
Werkzeug==3.1.9
```

ونفحص ملف القفل بدلاً من `requirements.txt`، ولاحظ أننا نخبر الأداة بنوع الملف لأن اسمه ليس الاسم المعتاد:

```bash
docker run --rm --name lab-asvs-osv -v "$PWD/fix:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 \
  scan source -L requirements.txt:/src/requirements.lock
```

```text
Scanned /src/requirements.lock file and found 12 packages
End status: 0 dirs visited, 1 inodes visited, 1 Extract calls, 2.257907ms elapsed, 2.258007ms wall time

No issues found
```

الأداة الآن ترى 12 حزمة بإصداراتها الدقيقة بدلاً من حزمتين، ولا توجد نتائج، وقد اختفت نتيجة `idna` لأن الملف يقول 3.20\. ثم Semgrep على الإصلاح، وهذه آخر سطور المخرج:

```bash
docker run --rm --name lab-asvs-semgrep -v "$PWD/fix:/src:ro" -w /src semgrep/semgrep:1.178.0 \
  semgrep scan --config p/python --config p/flask --metrics=off --exclude test_security.py --error
```

```text
✅ Scan completed successfully.
 • Findings: 0 (0 blocking)
 • Rules run: 151
 • Targets scanned: 1
 • Parsed lines: ~100.0%
 • Scan skipped:
   ◦ Matching --exclude patterns: 1
 • For a detailed list of skipped files and lines, run semgrep with the --verbose flag
Ran 151 rules on 1 file: 0 findings.
(need more rules? `semgrep login` for additional free Semgrep Registry rules)

If Semgrep missed a finding, please send us feedback to let us know!
See https://semgrep.dev/docs/reporting-false-negatives/
```

وأخيراً الاختبارات نفسها على المجلد `fix`، بالأمر السابق مع `cd fix` بدلاً من `cd pr`:

```text
....                                                                     [100%]
4 passed in 0.37s
```

### من وجد ماذا؟ الأدوات Tools والاختبارات Tests والمراجع Reviewer

الصورة التالية تجمع نتائج التجربة في جدول واحد:

![جدول من خمسة صفوف وأربعة أعمدة. الأعمدة Semgrep مع p/python وp/flask، وOSV-Scanner على requirements، وSecurity tests مع pytest، وHuman review مع القائمة. الصف 1 مسار جديد بلا فحص صلاحية GET /api/invoices/id IDOR: Semgrep لا، OSV ليس عمله، الاختبارات نعم بعد أن تطلبها المراجعة، المراجع نعم. الصف 2 التوكن يكتب في السجل: Semgrep جزئياً بسبب الكلمة فقط، الاختبارات نعم بـ caplog، المراجع نعم. الصف 3 استعلام SQL مبني بالنص: Semgrep نعم tainted-sql-string، الاختبارات نعم ببريد فيه علامة اقتباس، المراجع نعم. الصف 4 random لتوكن الاستعادة: Semgrep لا، الاختبارات لا لأن الفرق غير مرئي، المراجع نعم وهو الوحيد. الصف 5 حزمة جديدة عليها ثغرات معروفة requests 2.31.0: Semgrep لا، OSV-Scanner نعم بثلاث ثغرات، الاختبارات لا، المراجع جزئياً لأنه يسأل لماذا](https://arabroot.io/content/images/2026/10/asvs-review-04-human-vs-tools.webp)

كل أداة رأت جزءاً، والاختبارات لم توجد إلا لأن المتطلبات طلبتها، والعشوائية الضعيفة لم يرها إلا الإنسان

والخلاصة من الجدول أن أي طبقة وحدها كانت سوف تدمج مشكلة واحدة على الأقل: Semgrep وحدها تترك الصلاحيات والعشوائية والحزمة، وOSV-Scanner وحدها ترى الحزمة فقط، والاختبارات لا توجد أصلاً إلا إذا كتبت المتطلبات، والمراجع وحده قد يتعب أو يستعجل. والطبقات معاً تغطي الخمس، وهذا هو سبب أن تعمل القائمة مع الأدوات وليس بدلاً منها.

### إزالة التجربة Cleanup

كل الأوامر استخدمت `--rm`، فلا يبقى Container بعد انتهائه، ولم ننشئ أي Volume أو شبكة. وللتأكد:

```bash
docker ps -a --filter name=^lab-asvs- --format '{{.Names}}'
rm -rf ~/lab-asvs
```

وإذا بقي أي Container باسم يبدأ بـ `lab-asvs-` لسبب ما، فاحذفه بالاسم مع `docker rm -f`، ولا تستخدم أوامر التنظيف العامة مثل `docker system prune` لأنها تحذف ما يخص مشاريع أخرى على الجهاز.

## أخطاء شائعة Common Mistakes

- **لصق المعيار كاملاً في التذكرة:** 253 متطلباً في كل تذكرة يعني أن أحداً لن يقرأ أياً منها. اختر أقسام الميزة وما ينطبق منها، واكتب سبب الاستبعاد.
- **اختيار المستوى بحسب السهولة:** كما رأيت، المستوى الأول لا يمنع كتابة التوكنات في السجل، فالتطبيق الذي يحمل بيانات عملاء يحتاج المستوى الثاني على الأقل.
- **أرقام بلا إصدار:** كتابة «8.2.2» دون «v5.0.0» تجعل الرقم يتغير معناه مع الإصدار القادم، والمعيار نفسه يطلب ذكر الإصدار.
- **اعتبار غياب نتائج SAST موافقة:** الصلاحيات ومنطق العمل والعشوائية الضعيفة لم تظهر لأي أداة في التجربة.
- **تعليقات مراجعة عامة:** «هذا غير آمن» لا يصلح شيئاً، والتعليق المفيد فيه السطر والمتطلب والسيناريو والإصلاح.
- **اختبار الحالة المسموحة فقط:** اختبار «المالك يرى فاتورته» ينجح مع كود لا يفحص أي شيء، والاختبار الذي يحميك هو «غير المالك لا يراها».
- **CODEOWNERS لكل شيء:** المالك الذي يطلب في كل طلب دمج يوافق دون قراءة، فاجعل الملف للأجزاء الحساسة فقط وفعل موافقة المالك في حماية الفرع.
- **قائمة تكبر ولا تصغر:** القائمة الطويلة يضع الناس علامة على كل بنودها دون قراءة، فأضف بنداً عندما يتكرر خطأ واحذف بنداً لا يجد شيئاً.
- **فحص `requirements.txt` وحده:** الأداة قد تستنتج إصدارات غير التي تثبت، فافحص ملف القفل الذي يصف ما يعمل فعلاً.

## قائمة تحقق Checklist

- هل اخترت مستوى ASVS لكل تطبيق، مكتوباً مع السبب، والفصول المستبعدة مع السبب؟
- هل تحمل تذاكر الميزات الحساسة أرقام متطلبات بصيغة `v5.0.0-x.y.z` ومعايير قبول ممنوعة ومسموحة؟
- هل تتحول معايير القبول الأمنية إلى اختبارات تعمل في CI مع بقية الاختبارات؟
- هل يوجد ملف قالب طلب الدمج في `.gitea/` أو `.github/` وفيه القائمة الأمنية؟
- هل يعرف كل مراجع الأسئلة الثمانية وترتيبها؟
- هل يوجد ملف CODEOWNERS للمسارات الحساسة، وهل فرض موافقة المالك مفعل في حماية الفرع؟
- هل تعمل أدوات فحص الأسرار وSAST وSCA قبل المراجعة، وكل نتيجة تأخذ قراراً مكتوباً؟
- هل يوجد ملف قفل للاعتماديات ويفحص هو وليس ملف المتطلبات وحده؟
- هل يوجد سفير أمن في كل فريق يملك المسارات الحساسة؟
- إذا كانت عليك ضوابط NCA ECC أو ISO 27001: هل متطلبات ASVS التي اخترتها موثقة ومعتمدة، ونتائج التحقق منها محفوظة كدليل؟

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- جملة «اجعله آمناً» ليست متطلباً لأنها لا تنجح ولا تفشل، والمتطلب الأمني يكتب مثل المتطلب الوظيفي: محدد ومرقم وله اختبار.
- OWASP ASVS 5.0.0 (مايو 2025) فيه 345 متطلباً في 17 فصلاً، وثلاثة مستويات تراكمية: L1 بسبعين متطلباً كحد أدنى، وL2 بمئتين وثلاثة وخمسين لأغلب التطبيقات، وL3 للضمان العالي، والمستوى تختاره بحسب ما تحميه وليس بحسب سهولته.
- الرقم بصيغة `v5.0.0-8.2.2` يمر في التذكرة ومعايير القبول والكود والاختبار وتعليق المراجعة، ونمذجة التهديدات تكمل القائمة بما هو خاص بتصميمك.
- ASVS ليس شهادة ولا إطار امتثال، ولكنه مصدر جيد للمتطلبات الموثقة التي تطلبها ضوابط NCA ECC (1-6-3 و2-15) وضوابط التطوير الآمن في ISO 27001، وMASVS للجوال وSAMM لنضج البرنامج.
- مراجعة طلب الدمج أمنياً ثمانية أسئلة بالترتيب: ماذا تغير، وحدود الثقة، والمدخلات، والأماكن الخطرة، والصلاحيات، والأسرار والعشوائية، والسجلات والأخطاء، والاعتماديات والتهجير.
- القائمة تعيش في قالب طلب الدمج، وCODEOWNERS مع حماية الفرع يحضران صاحب الجزء الحساس، وسفير الأمن يملك هذه المسارات في الفريق الصغير.
- في التجربة وجدت Semgrep استعلام SQL، ونبهت على سطر السجل بسبب كلمة في الرسالة فقط، ووجدت OSV-Scanner ثلاث ثغرات في `requests` ونتيجة لإصدار لا يثبت، وكشفت الاختبارات المكتوبة من المتطلبات ثلاث مشكلات، ولم ير أحد غير الإنسان أن توكن الاستعادة مولد بـ `random`.

[![](https://arabroot.io/content/images/2026/10/owasp-top10-cover-cover.webp)دليلOWASP Top 10 للمبتدئين: أخطر عشر مشكلات في تطبيقات الويبأخطر عشر مشكلات أمنية في تطبيقات الويب، كل واحدة بمثال ضعيف ثم مصلح، وكيف تمنعها في الكود وأين تكتشفها.](https://arabroot.io/articles/owasp-top-10-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/threat-modeling-cover-cover.webp)دليلنمذجة التهديدات Threat Modeling للمبتدئينكيف تسأل ما الذي قد يسوء في تصميم تطبيقك قبل أن تكتب الكود، وتقرر لكل تهديد هل تصلحه أو تقبله، مع قالب جاهز للنسخ.](https://arabroot.io/articles/%D9%86%D9%85%D8%B0%D8%AC%D8%A9-%D8%A7%D9%84%D8%AA%D9%87%D8%AF%D9%8A%D8%AF%D8%A7%D8%AA-threat-modeling-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/devsecops-cover-cover.webp)دليلDevSecOps للمبتدئين: الأمن داخل خط CI/CD ومتى يعمل كل فحصما الذي يجده كل فحص أمني في خط CI/CD وما الذي يفوته، وكيف ترتب نتائجه حتى تصل إلى قرار بدل قائمة طويلة.](https://arabroot.io/articles/devsecops-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-sast-%D9%88-dast-%D9%88-sca/)[![](https://arabroot.io/content/images/2026/10/sast-cover-cover.webp)دليلSAST: كيف يجد تحليل الكود الثابت الثغرات قبل التشغيلكيف تقرأ أداة SAST الكود بحثاً عن الثغرات، وما الذي لا تراه أبداً، وكيف تمنع الإنذارات الخاطئة من أن تفقد الفريق ثقته بها.](https://arabroot.io/articles/%D8%AA%D8%AD%D9%84%D9%8A%D9%84-%D8%A7%D9%84%D9%83%D9%88%D8%AF-%D8%A7%D9%84%D8%AB%D8%A7%D8%A8%D8%AA-sast-%D9%85%D8%B9-semgrep-%D9%88-sonarqube/)[![](https://arabroot.io/content/images/2026/10/secure-coding-cover-cover.webp)دليلالبرمجة الآمنة Secure Coding بأمثلة من أربع لغاتكيف تكتب الكود الذي يتعامل مع مدخلات المستخدم والملفات والأخطاء بحيث لا يحتاج إصلاحاً أمنياً لاحقاً، بأمثلة في أربع لغات.](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D8%A7%D9%85%D9%86%D8%A9-secure-coding-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/testing-types-cover-cover.webp)دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، ومراجعة OWASP ASVS 5.0.0 من مستودعه الرسمي وعد متطلباته من ملف CSV الرسمي، ومراجعة الضوابط ECC-2:2024 ووثائق Gitea وGitHub وOWASP، وتنفيذ التجربة على Python 3.12.15 وFlask 3.1.3 وpytest 9.1.1 وSemgrep CE 1.178.0 وOSV-Scanner 2.6.0 داخل Docker Engine 29.8.0.