يعمل الفريق ثلاثة أشهر على ميزة جديدة، وقبل الإطلاق بأسبوع يرسل التطبيق إلى فريق الأمن أو إلى شركة فحص اختراق Penetration Testing، فيعود التقرير بأربعين ملاحظة، منها حقن SQL في صفحة البحث، ومفتاح API مكتوب في الكود منذ الشهر الأول، ومكتبة قديمة فيها ثغرة معروفة منذ سنتين، وال Docker Image مبنية على نظام انتهى دعمه. والآن أمام الفريق خياران كلاهما سيئ، إما أن يؤجل الإطلاق ليعيد كتابة أجزاء نسيها من كتبها، أو يطلق التطبيق ويضع الملاحظات في قائمة «سوف نصلحها لاحقاً» التي لا يعود إليها أحد. والمشكلة هنا ليست في التقرير، وإنما في توقيته، فكل ملاحظة فيه كان يمكن اكتشافها في اليوم الذي كتب فيه السطر، وبأداة تعمل آلياً في دقائق.
وهذا هو موضوع DevSecOps، أي أن يدخل الأمن إلى خط CI/CD نفسه فيعمل مع كل تغيير كما تعمل الاختبارات، بدل أن يكون بوابة واحدة في آخر الطريق. وهذا المقال هو المقال الرئيسي لسلسلة عن الموضوع، يعطيك الصورة الكاملة، ثم يرسلك إلى مقال مستقل لكل نوع من الفحوص.
وسوف نناقش في هذا المقال ما يلي:
- لماذا يفشل الأمن الذي يأتي في نهاية المشروع، وما معنى DevSecOps ونقل الفحص إلى اليسار Shift-Left.
- أنواع الفحوص الأمنية واحداً واحداً: الأسرار Secrets، والفحص الثابت SAST، وفحص الاعتماديات SCA، وقائمة المكونات SBOM، وفحص الحاويات والبنية ككود Container وIaC، والفحص أثناء التشغيل DAST، وIAST وRASP باختصار، واختبار الاختراق، ونمذجة التهديدات، مع ما يجده كل نوع وما يفوته.
- أين يعمل كل فحص في خط CI/CD، من جهاز المطور إلى الإنتاج Production، وكيف ترتبط هذه الفحوص بالاختبارات التي يكتبها فريقك.
- تجربة عملية على تطبيق صغير فيه سبع ثغرات مقصودة، نفحصه بأربع أدوات مفتوحة المصدر، ونرى أن كل أداة ترى جزءاً من التطبيق وتفوتها أجزاء.
- كيف تفرز النتائج: درجة الخطورة CVSS، واحتمال الاستغلال EPSS، وقائمة الثغرات المستغلة KEV، والإنذارات الخاطئة False Positives، والخط الأساسي Baseline، ومواعيد الإصلاح.
- الناس والثقافة: سفير الأمن Security Champion في كل فريق، والبوابة التي تمنع الدمج مقابل البوابة التي تنبه فقط، ثم خطة لإدخال الفحوص على مراحل، ومسار قراءة لبقية السلسلة.
لماذا يفشل الأمن Security الذي يأتي في النهاية؟
فحص الاختراق Penetration Test قبل الإطلاق فقط
الطريقة الأولى التي تبدأ بها أغلب الفرق هي ما وصفناه في البداية، أي أن يكتب الفريق الكود كما يريد، ثم يأتي من يفحصه مرة واحدة قبل الإطلاق. وهذه الطريقة أفضل من لا شيء، لكنها تفشل لثلاثة أسباب، فالأول أن الفحص يأتي متأخراً، فالثغرة التي كان إصلاحها تعديل سطر واحد وقت كتابته أصبحت مدفونة تحت عشرات التغييرات التي تعتمد عليها، والثاني أنه يحدث مرة واحدة، أما الكود فيتغير كل يوم، فالتطبيق الذي فحص في مارس ليس هو التطبيق الذي يعمل في يونيو، والثالث أن الثغرات المعروفة في المكتبات تنشر كل يوم، فالمكتبة التي كانت سليمة يوم الفحص قد تظهر فيها ثغرة بعد أسبوع دون أن يتغير في كودك حرف واحد.
فريق الأمن كبوابة أخيرة Security Gate
والحل الثاني الذي يخطر على البال هو أن تعين فريقاً للأمن يراجع كل إصدار قبل نشره، وهذا أفضل لأنه يتكرر، لكنه يصطدم بالأرقام، ففي أغلب الشركات يكون عدد المطورين أكبر بكثير من عدد مختصي الأمن، فيتحول الفريق إلى طابور طويل، ويتعلم المطورون أن الأمن هو «القسم الذي يؤخر الإطلاق»، فيبدؤون في البحث عن طرق للالتفاف عليه، والأسوأ أن المطور لا يتعلم شيئاً، لأنه يستلم قائمة ملاحظات بعد أسابيع من كتابة الكود، فيصلحها دون أن يفهم لماذا حدثت، ويكررها في الميزة التالية.
تكلفة الإصلاح Cost of Fixing تزيد مع كل مرحلة
والصورة التالية تبين الفكرة التي تجمع هذه المشاكل، وهي أن الخطأ نفسه يكلف أكثر كلما تأخر اكتشافه:

لاحظ أننا لم نضع أرقاماً على المحور الرأسي، والسبب أن الأرقام المتداولة لهذه الفكرة، مثل «الإصلاح في الإنتاج يكلف مئة ضعف»، يصعب تتبعها إلى دراسة واضحة يمكن الاعتماد عليها، لذلك نكتفي بالمنطق الذي يعرفه كل مطور من عمله اليومي، فإصلاح سطر وأنت تكتبه يأخذ دقيقة، وإصلاحه بعد النشر يحتاج بناء وإصداراً جديدين وتنسيقاً مع فرق أخرى، وإذا استغله أحد قبل أن تصلحه فقد أصبح حادثة فيها بيانات عملاء وإبلاغ وتحقيق.
ما هو DevSecOps، وما معنى Shift-Left؟
DevSecOps هو أن يصبح الأمن جزءاً من طريقة عمل DevOps نفسها، فتعمل الفحوص الأمنية آلياً في خط CI/CD مع كل تغيير، وتظهر نتيجتها للمطور في المكان الذي يعمل فيه، أي في طلب الدمج Pull Request وفي محرر الكود، ويتحمل الفريق الذي يكتب الكود مسؤولية أمانه، بينما يتحول فريق الأمن من بوابة في النهاية إلى من يضع القواعد ويختار الأدوات ويساعد في الحالات الصعبة. ويلخص دليل OWASP DevSecOps Guideline الفكرة في جملة واحدة، وهي أن تكتشف المشاكل الأمنية، سواء كانت أخطاء في التصميم أو ثغرات في التطبيق، في أبكر وقت ممكن وبأقل تكلفة ممكنة، دون أن تبطئ سرعة CI/CD.
ونقل الفحص إلى اليسار Shift-Left هو الاسم الذي يطلق على هذه الفكرة، والسبب أن خط التطوير يرسم عادة من اليسار (التصميم والكود) إلى اليمين (الإنتاج)، فنقل الفحص إلى اليسار يعني أن يحدث أبكر، والتشبيه الأقرب هو مراقبة الجودة في مصنع، فالمصنع الذي يفحص كل قطعة على خط الإنتاج يرمي قطعة واحدة معيبة، أما المصنع الذي يفحص المنتج النهائي فقط فيكتشف أن آلاف القطع خرجت معيبة منذ أسبوع.
وقد يتساءل البعض: هل يعني هذا أن نوقف الفحص في اليمين، أي في الإنتاج؟ والإجابة لا، فبعض الأشياء لا تظهر إلا والتطبيق يعمل، مثل ترويسات الاستجابة الناقصة والإعداد الخاطئ للسيرفر، وبعضها يظهر بعد النشر بأشهر، مثل ثغرة جديدة تنشر في مكتبة تستخدمها، لذلك تتحدث المراجع عن الفحص في اليسار مع الحماية والمراقبة في اليمين، فالهدف أن يحدث كل فحص في أبكر مكان يستطيع أن يرى فيه المشكلة، وليس أن تحدث كل الفحوص في مكان واحد. وفي الإطار الأمريكي NIST SSDF (SP 800-218) لتطوير البرمجيات الآمنة ستجد الفكرة نفسها مكتوبة كممارسات: حماية الكود، وفحصه آلياً، ومعالجة الثغرات بعد الإصدار.
أنواع الفحوص الأمنية Security Checks واحداً واحداً
كل فحص من الفحوص التالية ينظر إلى طبقة مختلفة من التطبيق، فبعضها يقرأ كودك، وبعضها يقرأ قائمة المكتبات، وبعضها يفتح ال Docker Image، وبعضها يهاجم التطبيق وهو يعمل، والصورة التالية تبين ذلك:

فحص الأسرار Secrets Scanning
يبحث في الكود وفي تاريخ Git كله عن كلمات مرور ومفاتيح API وتوكنات ومفاتيح خاصة Private Keys كتبت في الملفات، وذلك بقواعد تعرف شكل مفاتيح الخدمات المشهورة، وبقياس العشوائية Entropy للنصوص التي تشبه المفاتيح. وأهم ما يميزه أن الضرر يحدث لحظة الدفع Push، فالسر الذي وصل إلى المستودع يجب اعتباره مسرباً حتى لو حذفته في ال commit التالي، لأنه بقي في التاريخ وفي كل نسخة Clone منه.
- ما يجده: المفاتيح التي لها شكل معروف بدقة عالية، والنصوص العشوائية الطويلة التي يسبقها اسم مثل
tokenأوapi_key. - ما يفوته: كلمة المرور التي تشبه كلاماً عادياً، والأسرار في أماكن لا يقرؤها، مثل متغيرات بيئة على السيرفر أو رسالة في نظام المحادثة.
- أدواته: Gitleaks وTruffleHog، وشرحناها في اكتشاف الأسرار المسربة مع Gitleaks وTruffleHog، مع ما تفعله عندما يتسرب مفتاح فعلاً.
الفحص الثابت للكود SAST
اختصار Static Application Security Testing، وهو أداة تقرأ الكود دون أن تشغله، فتبني منه شجرة Abstract Syntax Tree وتتبع البيانات من مصدرها Source، مثل مدخلات المستخدم في request.args، إلى مكان خطر Sink، مثل دالة تنفذ SQL، فإذا وصلت البيانات إلى المكان الخطر دون تنظيف أبلغت عن ثغرة، مع رقم السطر.
- ما يجده: حقن SQL Injection والأوامر Command Injection وXSS في الكود الذي تكتبه أنت، والدوال الخطرة، والإعدادات الخطرة في الكود.
- ما يفوته: الثغرات في المكتبات، وأخطاء المنطق والصلاحيات مثل «مستخدم يرى بيانات غيره»، لأن الأداة لا تعرف ما الذي يجب أن يراه كل مستخدم، وكل ما يعتمد على الإعداد وقت التشغيل.
- عيبه المعروف: الإنذارات الخاطئة، لأن الأداة لا تعرف السياق، وسوف ترى مثالاً لذلك في التجربة.
- أدواته: Semgrep وSonarQube وCodeQL وأدوات خاصة بكل لغة، في تحليل الكود الثابت SAST.
فحص الاعتماديات SCA
اختصار Software Composition Analysis، وهو فحص المكتبات التي يعتمد عليها تطبيقك، المباشرة منها والمكتبات التي تعتمد عليها هذه المكتبات Transitive Dependencies، فيقرأ ملف الاعتماديات أو ملف القفل Lockfile، ويقارن كل اسم وإصدار بقاعدة بيانات للثغرات المعروفة مثل OSV. ولاحظ أن أغلب كود تطبيقك الحديث ليس كودك، فعشرة أسطر في requirements.txt قد تجلب عشرات المكتبات.
- ما يجده: الإصدارات التي لها ثغرات منشورة برقم CVE أو معرف مثل GHSA، ومعها الإصدار الذي يصلحها، وبعض الأدوات تفحص التراخيص أيضاً.
- ما يفوته: الثغرات التي لم تنشر بعد، والمكتبة الخبيثة التي لم يبلغ عنها أحد، وهو لا يعرف في الغالب هل يستدعي كودك الدالة المصابة فعلاً أم لا، فيعطيك قائمة طويلة تحتاج إلى فرز.
- أدواته: OSV-Scanner وDependency-Track وTrivy وGrype، وتحديث المكتبات مع Renovate، في فحص الاعتماديات SCA.
قائمة مكونات البرنامج SBOM
اختصار Software Bill of Materials، وهي ليست فحصاً بذاتها، وإنما قائمة مكتوبة بصيغة قياسية بكل مكون داخل ما بنيته وإصداره، مثل قائمة مكونات المنتج الغذائي. وفائدتها أنها تجيب عن السؤال الذي يأتي يوم تنشر ثغرة كبيرة: «أي تطبيقاتنا تستخدم هذه المكتبة، وبأي إصدار؟» دون أن تبحث في كل مستودع. وقد شرحناها مع التوقيع ومخاطر سلسلة التوريد في أمن سلسلة التوريد البرمجية، ولن نعيد ذلك هنا.
فحص الحاويات والبنية ككود Container وIaC
فحص ال Docker Image يفتح طبقاتها ويقرأ حزم نظام التشغيل فيها، مثل حزم Debian أو Alpine، ومكتبات اللغة المثبتة داخلها، ويقارنها بقواعد الثغرات، وهذا يجد ما لا يراه فحص الاعتماديات، لأن Base Image التي اخترتها قد تحمل مئات الحزم التي لم تكتبها في أي ملف. أما فحص البنية ككود Infrastructure as Code فيقرأ ملفات Dockerfile وKubernetes وTerraform ويبحث عن إعداد خطر، مثل تشغيل ال Container بصلاحيات root، أو منفذ قاعدة بيانات مفتوح للعالم.
- ما يجده: الحزم القديمة في ال Image، وال Base Image التي انتهى دعمها، والإعداد الخطر في الملفات.
- ما يفوته: منطق تطبيقك كله، وما يحدث في الإنتاج إذا عدل أحدهم الإعداد يدوياً.
- أدواته: Trivy وGrype وHadolint وCheckov، في فحص الحاويات والبنية ككود مع Trivy.
الفحص أثناء التشغيل DAST
اختصار Dynamic Application Security Testing، وهو أداة تعامل التطبيق كصندوق أسود Black Box، فلا ترى الكود، وإنما ترسل طلبات HTTP إلى التطبيق وهو يعمل، كما يفعل المخترق، فتزحف على الصفحات، وتضع نصوصاً خاصة في كل حقل، وتقرأ الردود والترويسات.
- ما يجده: ما يظهر من الخارج فعلاً، مثل XSS المنعكس، وترويسات الأمان الناقصة، وملفات تعريف الارتباط بلا
SecureأوHttpOnly، وإعادة التوجيه المفتوحة، ورسائل الخطأ التي تكشف تفاصيل السيرفر، وهو لا يهتم بلغة البرمجة. - ما يفوته: الصفحات التي لا يصل إليها، مثل ما يحتاج تسجيل دخول إذا لم تعطه حساباً، وهو لا يعطيك رقم السطر، ويحتاج إلى بيئة تعمل، لذلك يأتي متأخراً في الخط ويستغرق وقتاً أطول.
- أدواته: ZAP وNuclei، على بيئة المعاينة، في فحص التطبيق أثناء التشغيل DAST. ولا تقم بتشغيله على موقع لا تملكه، والسبب أنه هجوم حقيقي في نظر صاحب الموقع وفي نظر القانون.
IAST وRASP باختصار
بين الفحص الثابت والفحص من الخارج يوجد نوعان يعملان من داخل التطبيق وهو يعمل. فالأول IAST، أي Interactive Application Security Testing، يضع وكيلاً Agent داخل التطبيق أثناء الاختبارات، فيرى الطلب القادم والسطر الذي نفذه معاً، ويعطيك نتائج أدق مع رقم السطر، لكنه مرتبط باللغة وأغلب أدواته تجارية. والثاني RASP، أي Runtime Application Self-Protection، يعمل داخل التطبيق في الإنتاج ويوقف الطلب الخطر لحظة تنفيذه، فهو أقرب إلى الحماية منه إلى الفحص. وكلاهما خطوة متقدمة، ولا ننصح بالبدء بهما قبل أن تعمل الفحوص الأساسية.
اختبار الاختراق Penetration Testing
إنسان مختص يحاول اختراق التطبيق بالطريقة التي يفكر بها المهاجم، فيربط ثغرتين صغيرتين ليصل إلى شيء كبير، ويجرب منطق العمل نفسه، مثل «هل أستطيع أن أطلب منتجاً بسعر سالب؟». وهذا ما لا تستطيعه أي أداة آلية، لذلك لا يلغيه DevSecOps، وإنما يغير دوره، فبدل أن يقضي المختبر أيامه في إيجاد مكتبة قديمة كانت الأداة ستجدها في دقيقة، يقضيها في أخطاء المنطق والصلاحيات التي لا تراها الأدوات. ويكون عادة دورياً، مرة أو مرتين في السنة، وقبل الإطلاقات الكبيرة.
نمذجة التهديدات Threat Modeling
وهي الوحيدة في القائمة التي تحدث قبل أن يوجد كود، فيجلس الفريق أمام مخطط الميزة الجديدة ويسأل «ما الذي قد يسوء هنا؟»، والخطأ الذي تكتشفه في المخطط يكلف تعديل رسمة، ولا توجد أداة تجده لك لاحقاً إذا كان القرار نفسه خاطئاً، مثل ميزة صممت بحيث يرسل الكلاينت سعر المنتج. وقد شرحناها بالتفصيل في نمذجة التهديدات Threat Modeling للمبتدئين.
الفحوص Checks في جدول واحد
| الفحص | ما يقرؤه | يجد | يفوته | الزمن التقريبي |
|---|---|---|---|---|
| الأسرار Secrets | الملفات وتاريخ Git | المفاتيح والتوكنات | الأسرار التي تشبه كلاماً عادياً | ثوان |
| SAST | كودك | الحقن وXSS والدوال الخطرة | المكتبات والمنطق والصلاحيات | ثوان إلى دقائق |
| SCA | ملف الاعتماديات | الثغرات المعروفة في المكتبات | غير المنشور، وهل يستدعى الكود المصاب | ثوان |
| Container وIaC | ال Image وملفات الإعداد | حزم النظام القديمة والإعداد الخطر | منطق التطبيق | دقيقة تقريباً |
| DAST | التطبيق أثناء التشغيل | ما يظهر في الردود والترويسات | رقم السطر، وما لا يصل إليه | دقائق إلى ساعات |
| اختبار الاختراق | كل شيء، بعقل إنسان | المنطق وربط الثغرات | ما تغير بعد انتهائه | أيام |
| نمذجة التهديدات | التصميم | أخطاء القرار نفسه | أخطاء التنفيذ | ساعة لكل ميزة |
أين يعمل كل فحص في خط CI/CD؟
القاعدة هنا هي نفسها التي ذكرناها مع الاختبارات، فالفحص الأسرع يعمل أبكر وأكثر، والأبطأ يعمل لاحقاً وأقل، ولكن مع شرط إضافي، وهو أن كل فحص يحتاج إلى أن يكون الشيء الذي يفحصه موجوداً، فلا معنى لفحص ال Docker Image قبل أن تبنى، ولا لفحص DAST قبل أن يعمل التطبيق. والصورة التالية تبين المكان المعتاد لكل فحص:

- قبل ال commit على جهاز المطور: فحص الأسرار عبر Git Hook، لأن السر الذي لا يدخل إلى ال commit لا يحتاج إلى تدوير، ومعه قواعد بسيطة في محرر الكود. والشرط أن يأخذ ثواني، ولأن المطور يستطيع تجاوز ال Hook على جهازه فإن الفحص نفسه يتكرر في الخط.
- على كل طلب دمج: الأسرار مرة ثانية، وSAST على الملفات التي تغيرت، وSCA على ملف الاعتماديات، وفحص ملفات Dockerfile وIaC. وهذا هو المكان الأهم في السلسلة كلها، والسبب أن النتيجة تظهر للمطور وهو ما زال يتذكر ما كتبه، وأن المراجع يراها بجانب الكود.
- عند البناء: تبنى ال Docker Image مرة واحدة، فتفحص، وتكتب لها SBOM، وتوقع Signing، ثم تنتقل كما هي بين البيئات كما شرحنا في ابن ال Docker Image مرة واحدة.
- على بيئة المعاينة أو staging: DAST على التطبيق وهو يعمل، ويكون عادة ليلياً أو على الطلبات المهمة لأنه أبطأ، ويعمل على بيئة المعاينة وليس على الإنتاج.
- قبل النشر: بوابة تقرر حسب سياسة مكتوبة، مثل «لا تنشر Image فيها ثغرة Critical لها إصلاح، أو ثغرة في قائمة KEV»، والتحقق من أن ال Image الموقعة هي نفسها التي بنيت.
- في الإنتاج: إعادة فحص ال Images التي تعمل كل يوم، لأن ثغرات جديدة تنشر كل يوم في حزم لم تتغير، ومعها اختبار الاختراق الدوري، وبلاغات الباحثين، والسجلات، وطبقة الحماية أمام التطبيق التي شرحناها في حماية تطبيقات الويب على مستوى HTTP.
وقد شرحنا تشغيل خط CI على سيرفرك في دليل Gitea Actions لتشغيل CI/CD على السيرفر، وفي آخر السلسلة يربط مقال خط CI/CD آمن من البداية للنهاية كل هذه الفحوص في خط واحد يعمل، مع حماية الخط نفسه، لأن خط CI الذي يملك مفاتيح النشر هدف بذاته.
كيف ترتبط الفحوص الأمنية Security Checks بالاختبارات Tests التي تكتبها؟
في مقال أنواع الاختبارات للمبتدئين رأيت أن كل نوع من الاختبارات يرى طبقة من التطبيق، وأن ثلاثة أخطاء في التجربة لم تكشف كل منها إلا طبقة واحدة، والفكرة نفسها تنطبق هنا تماماً، فالفحوص الأمنية طبقات أخرى فوق الاختبارات وليست بديلاً عنها، والفرق بينهما في السؤال الذي يجيب عنه كل منهما:
- الاختبار يسأل «هل يعمل التطبيق كما يجب؟»: وأنت تعرف الجواب الصحيح وتكتبه، لذلك هو الأفضل في أخطاء المنطق والصلاحيات. واختبار ال API الذي يتأكد أن مستخدماً لا يرى طلبات غيره هو اختبار أمني في الحقيقة، ولا تجده أي أداة من الأدوات أعلاه، لأنها لا تعرف قواعد عملك، وقد ذكرنا ذلك في قسم اختبارات الأمان من ذلك المقال.
- الفحص الأمني يسأل «هل في التطبيق نمط خطر معروف؟»: ولا يحتاج منك أن تكتب الجواب، لأن الأداة تعرف آلاف الأنماط والثغرات المنشورة مسبقاً، لذلك هو الأفضل فيما لم يخطر على بالك.
- كل ثغرة تصلحها تصبح اختبار انحدار Regression Test: فإذا وجد DAST أو اختبار الاختراق ثغرة XSS في صفحة البحث، فاكتب اختباراً يرسل النص نفسه ويتأكد أنه يعود مهرباً Escaped، حتى لا تعود الثغرة مع تعديل قادم.
- الاختبارات التي يكتبها الذكاء الاصطناعي: النموذج الذي يكتب اختبارات من الكود الموجود ينسخ سلوكه بأخطائه، كما رأيت في أتمتة الاختبارات بالذكاء الاصطناعي، وهذا ينطبق على الثغرات أيضاً، فالاختبار المولد من دالة فيها حقن SQL سوف يتأكد أن الحقن يعمل. وفي المقال نفسه مخاطر إرسال الكود والأسرار إلى المساعد وحقن التعليمات Prompt Injection.
- اختبار الأحمال ليس فحصاً أمنياً لكنه قريب منه: فالتطبيق الذي يسقط تحت ضغط مئة طلب في الثانية سوف يسقطه مهاجم بسهولة، وما شرحناه في اختبار الأحمال والضغط يعطيك حدود تطبيقك قبل أن يعطيك إياها غيرك، أما الحد من معدل الطلبات Rate Limiting فهو طبقة الحماية المقابلة له.
تجربة عملية Lab: تطبيق واحد وأربع أدوات، وكل أداة ترى جزءاً
الكلام عن «كل أداة ترى طبقة» يصبح أوضح عندما تراه في مخرجات حقيقية، لذلك سوف نأخذ تطبيقاً صغيراً فيه سبع ثغرات وضعناها عن قصد، ونشغل عليه أربع أدوات مفتوحة المصدر، ثم نرى أي ثغرة وجدتها كل أداة وأيها فاتها. وهذا التطبيق نفسه هو الذي سوف نستخدمه في بقية مقالات السلسلة، فكل مقال يأخذه ويصلح جزءاً منه.
التطبيق التجريبي Demo App وثغراته السبع
التطبيق متجر صغير مكتوب ب Python وFlask مع قاعدة بيانات SQLite، فيه صفحة تسجيل دخول، وصفحة بحث، ومسار إعادة توجيه، ومسار API محمي بتوكن، وملف Dockerfile وملف requirements.txt. وكل ثغرة معلمة في الكود بتعليق VULN-xx، والتطبيق كاملاً في مستودع الموقع في المجلد content/labs/devsecops-app/، وهذه أهم أجزائه:
app.config["SECRET_KEY"] = "flask-dev-secret-2024" # VULN-01
ADMIN_API_TOKEN = "dso_live_7Hq2Lx9Vt4Rz8Wn3Bk6Mh1Sd5Gj0CyPa" # VULN-01
@app.route("/search")
def search():
q = request.args.get("q", "")
# VULN-02: SQL injection
rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall()
items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows)
# VULN-03: reflected XSS
return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>")
@app.route("/go")
def go():
# VULN-05: open redirect
return redirect(request.args.get("next", "/"))
# VULN-04: missing security headers (no after_request hook adds them)والملفان الآخران كما يلي:
# requirements.txt (VULN-06)
Flask==2.2.2
Werkzeug==2.2.2
Jinja2==3.1.2
# Dockerfile (VULN-07)
FROM python:3.8-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]في الإعداد أعلاه لاحظ التالي:
- VULN-01: سران مكتوبان في الكود، مفتاح توقيع الجلسات
SECRET_KEYوتوكن ال API، وكلاهما سوف يبقى في تاريخ Git وفي ال Docker Image. - VULN-02: نص البحث يدخل إلى جملة SQL مباشرة عبر f-string، والحل الصحيح هو الاستعلام بالمعاملات Parameterized Query كما في ورقة OWASP.
- VULN-03: نص البحث يعود في صفحة HTML دون تهريب Escaping، وهذا XSS منعكس Reflected XSS.
- VULN-04: لا توجد ترويسات أمان مثل
Content-Security-PolicyوX-Content-Type-Options، وشرحناها في ترويسات الأمان HSTS وCSP. - VULN-05: المسار
/go?next=يحول المستخدم إلى أي موقع، فيستخدمه المحتال في رابط يبدأ باسم نطاقك، وهذا ما يسمى CWE-601 Open Redirect. - VULN-06: ثلاث مكتبات بإصدارات من سنة 2022.
- VULN-07: Base Image مبنية على Debian 10 buster الذي انتهى دعمه، ولا يوجد سطر
USER، فيعمل التطبيق بصلاحيات root داخل ال Container.
تشغيل التطبيق ورؤية الثغرات Vulnerabilities بأنفسنا
قبل أن نسأل الأدوات، نتأكد أن الثغرات حقيقية، فنبني ال Image ونشغل التطبيق على العنوان 127.0.0.1 فقط، ثم نجرب كل ثغرة بالأمر curl:
cd content/labs/devsecops-app
docker build -t lab-dso-hub-app:demo .
docker run -d --name lab-dso-hub-app -p 127.0.0.1:19880:8000 lab-dso-hub-app:demo
# VULN-02: حقن SQL يعرض جدول المستخدمين
curl -s "http://127.0.0.1:19880/search?q=%27%20UNION%20SELECT%20username%2C%20password_hash%20FROM%20users%20--"والمخرج سوف يكون كما يلي:
<!doctype html><html><head><meta charset='utf-8'><title>Demo Shop</title></head><body><h1>Results for ' UNION SELECT username, password_hash FROM users --</h1><ul><li>Keyboard: 120.0</li><li>Monitor: 899.0</li><li>Mouse: 45.5</li><li>admin: pbkdf2:sha256:260000$C3FxCbzeI6AchtHp$1db13a48ce26fa7d2685ae39a533c30fff285c91d934f6149b88c036c70908fd</li></ul></body></html>لاحظ السطر الأخير في القائمة، فصفحة بحث المنتجات أعادت اسم المستخدم admin وهاش كلمة مروره، وهذا بالضبط ما يفعله حقن SQL. ثم نجرب بقية الثغرات:
# VULN-03: النص يعود في الصفحة كما هو
curl -s "http://127.0.0.1:19880/search?q=%3Cscript%3Ealert(1)%3C/script%3E"
# VULN-04 وVULN-05: الترويسات، والتحويل إلى موقع آخر
curl -sI "http://127.0.0.1:19880/go?next=https://evil.example"
# VULN-07: المستخدم داخل ال Container
docker exec lab-dso-hub-app idوالمخرج سوف يكون كما يلي:
<!doctype html><html><head><meta charset='utf-8'><title>Demo Shop</title></head><body><h1>Results for <script>alert(1)</script></h1><ul></ul></body></html>
HTTP/1.1 302 FOUND
Server: Werkzeug/2.2.2 Python/3.8.17
Date: Tue, 06 Oct 2026 07:16:10 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 227
Location: https://evil.example
Connection: close
uid=0(root) gid=0(root) groups=0(root)في المخرج أعلاه لاحظ التالي:
- الوسم
<script>عاد في الصفحة كما أرسلناه، فالمتصفح سوف ينفذه. - الرد
302يحول إلىhttps://evil.exampleدون أي تحقق، ولا توجد في الترويسات أي ترويسة أمان، بينما الترويسةServerتكشف إصدار Werkzeug وPython. - الأمر
idيؤكد أن التطبيق يعمل بصلاحيات root.
الأداة الأولى: Gitleaks لفحص الأسرار Secrets
نبدأ بالأسرار، وهي أسرع الفحوص، باستخدام Gitleaks (ترخيص MIT) في Container يقرأ المجلد للقراءة فقط، والأمر dir يفحص الملفات كما هي، أما فحص تاريخ Git فهو موضوع مقال الأسرار:
docker run --rm -v "$PWD:/src:ro" ghcr.io/gitleaks/gitleaks:v8.30.1 dir /src --no-banner -vوالمخرج سوف يكون كما يلي:
Finding: ADMIN_API_TOKEN = "dso_live_7Hq2Lx9Vt4Rz8Wn3Bk6Mh1Sd5Gj0CyPa"
Secret: dso_live_7Hq2Lx9Vt4Rz8Wn3Bk6Mh1Sd5Gj0CyPa
RuleID: generic-api-key
Entropy: 5.259991
File: /src/app.py
Line: 17
Fingerprint: /src/app.py:generic-api-key:17
7:45AM INF scanned ~9820 bytes (9.82 KB) in 522ms
7:45AM WRN leaks found: 1وجد Gitleaks التوكن بالقاعدة العامة generic-api-key لأن قيمته عشوائية جداً، فدرجة العشوائية Entropy فيها 5.26، وأعاد رمز الخروج 1، وهو ما يجعل خط CI يفشل. لكن لاحظ ما لم يجده، فمفتاح الجلسات SECRET_KEY في السطر الذي قبله لم يظهر، لأن قيمته flask-dev-secret-2024 لم تطابق قواعد الأداة، وهي في الواقع أخطر من التوكن، لأن من يعرفها يستطيع أن يوقع ملف تعريف ارتباط Cookie لأي مستخدم، كما شرحنا في الجلسات وملفات تعريف الارتباط وJWT.
الأداة الثانية: Semgrep للفحص الثابت SAST
ثم الفحص الثابت باستخدام Semgrep، والمحرك نفسه بترخيص LGPL-2.1، أما القواعد في سجل Semgrep فلها ترخيص خاص بها يسمح باستخدامها داخل مؤسستك ويمنع بيعها في منتج منافس. ونطلب ثلاث مجموعات من القواعد العامة: Python وFlask وDockerfile، ونضيف --error حتى يعيد رمز خروج 1 عند وجود نتائج، و--emacs حتى تكون كل نتيجة في سطر واحد:
docker run --rm -v "$PWD:/src:ro" -w /src semgrep/semgrep:1.178.0 \
semgrep scan --config p/python --config p/flask --config p/dockerfile \
--metrics=off --error --emacs --quietوالمخرج سوف يكون كما يلي:
Dockerfile:12:1:error(missing-user):CMD ["python", "app.py"]:By not specifying a USER, a program in the container may run as 'root'. This is a security hazard. If an attacker can control a process running as root, they may have control over the container. Ensure that the last USER in a Dockerfile is a USER other than 'root'.
app.py:86:5:warning(sql-injection-db-cursor-execute): q = request.args.get("q", ""):User-controlled data from a request is passed to 'execute()'. This could lead to a SQL injection and therefore protected information could be leaked. Instead, use django's QuerySets, which are built with query parameterization and therefore not vulnerable to sql injection. For example, you could use `Entry.objects.filter(date=2006)`.
app.py:90:25:error(tainted-sql-string): rows = db().execute(f"SELECT name, price FROM products WHERE name LIKE '%{q}%'").fetchall():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:91:21:warning(raw-html-format): items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows):Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`django.shortcuts.render`) which will safely render HTML instead.
app.py:91:21:warning(raw-html-format): items = "".join(f"<li>{r[0]}: {r[1]}</li>" for r in rows):Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`flask.render_template`) which will safely render HTML instead.
app.py:94:5:warning(directly-returned-format-string): return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>"):Detected Flask route directly returning a formatted string. This is subject to cross-site scripting if user input can reach the string. Consider using the template engine instead and rendering pages with 'render_template()'.
app.py:94:12:warning(raw-html-format): return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>"):Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`django.shortcuts.render`) which will safely render HTML instead.
app.py:94:12:warning(raw-html-format): return PAGE.format(f"<h1>Results for {q}</h1><ul>{items}</ul>"):Detected user input flowing into a manually constructed HTML string. You may be accidentally bypassing secure methods of rendering HTML by manually constructing HTML and this could create a cross-site scripting vulnerability, which could let attackers steal sensitive user data. To be sure this is safe, check that the HTML is rendered safely. Otherwise, use templates (`flask.render_template`) which will safely render HTML instead.
app.py:124:5:warning(avoid_app_run_with_bad_host): app.run(host="0.0.0.0", port=8000):Running flask app with host 0.0.0.0 could expose the server publicly.في المخرج أعلاه لاحظ التالي:
- VULN-02 وجدها: القاعدة
tainted-sql-stringتتبعت النص منrequest.argsإلى جملة SQL في السطر 90، وهذا تتبع المصدر إلى المكان الخطر الذي ذكرناه. - VULN-03 وجدها: في السطر 94 بثلاث قواعد مختلفة، وفي السطر 91 أيضاً، لأن نتائج قاعدة البيانات نفسها قد تحمل نصاً كتبه مستخدم.
- VULN-07 وجد نصفها: القاعدة
missing-userلاحظت غياب سطرUSER، لكنها لا تعرف شيئاً عن عمر ال Base Image. - التكرار: تسع نتائج لثلاث ثغرات، والسبب أن مجموعة Python تحتوي قواعد Django أيضاً، فظهرت النتيجة نفسها مرة باسم Django ومرة باسم Flask، وتوصية Django في السطر الثاني لا تنفع تطبيق Flask أصلاً.
- إنذار خاطئ: النتيجة الأخيرة تقول إن الاستماع على
0.0.0.0قد يكشف السيرفر، وهذا صحيح على جهاز عادي، لكن داخل Container فهو الإعداد المطلوب، لأن التطبيق لن يستقبل أي طلب من خارج ال Container إذا استمع على127.0.0.1، والذي يحدد من يصل إليه هو ربط المنفذ-p 127.0.0.1:19880:8000. وهذا نموذج لإنذار خاطئ تعالجه بتعليق يتجاهل القاعدة في السطر نفسه مع السبب، كما يشرح توثيق Semgrep. - ما فاته: السران في VULN-01 لم يظهرا مع هذه المجموعات، ولا إعادة التوجيه المفتوحة VULN-05 رغم أنها سطر واحد واضح، ولا الترويسات الناقصة VULN-04، ولا المكتبات القديمة VULN-06، لأن Semgrep يقرأ كودك فقط.
الأداة الثالثة: OSV-Scanner لفحص الاعتماديات Dependencies
ثم المكتبات باستخدام OSV-Scanner من Google (ترخيص Apache-2.0)، وهو يقرأ ملف requirements.txt ويسأل قاعدة بيانات OSV:
docker run --rm -v "$PWD:/src:ro" ghcr.io/google/osv-scanner:v2.6.0 scan source -r /srcوالمخرج سوف يكون كما يلي، وقد طبع الإصدار 2.6.0 الجدول كاملاً مرتين متطابقتين، فأبقينا نسخة واحدة منه:
Scanning dir /src
Starting filesystem walk for root: /
Scanned /src/requirements.txt file and found 3 packages
End status: 1 dirs visited, 6 inodes visited, 1 Extract calls, 104.679822ms elapsed, 104.680022ms wall time
Total 6 packages affected by 34 known vulnerabilities (0 Critical, 14 High, 18 Medium, 2 Low, 0 Unknown) from 1 ecosystem.
34 vulnerabilities can be fixed.
+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+
| OSV URL | CVSS | ECOSYSTEM | PACKAGE | VERSION | FIXED VERSION | SOURCE |
+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+
| https://osv.dev/PYSEC-2023-62 | 8.7 | PyPI | flask | 2.2.2 | 2.2.5 | src/requirements.txt |
| https://osv.dev/GHSA-m2qf-hxjv-5gpq | | | | | | |
| https://osv.dev/PYSEC-2026-2151 | 4.3 | PyPI | flask | 2.2.2 | 3.1.3 | src/requirements.txt |
| https://osv.dev/GHSA-68rp-wp8r-4726 | | | | | | |
| https://osv.dev/PYSEC-2026-1471 | 5.4 | PyPI | jinja2 | 3.1.2 | 3.1.6 | src/requirements.txt |
| https://osv.dev/GHSA-cpwx-vrp4-4pq7 | | | | | | |
| https://osv.dev/PYSEC-2026-1472 | 8.8 | PyPI | jinja2 | 3.1.2 | 3.1.5 | src/requirements.txt |
| https://osv.dev/GHSA-gmj6-6f8f-6699 | | | | | | |
| https://osv.dev/PYSEC-2026-1473 | 5.4 | PyPI | jinja2 | 3.1.2 | 3.1.3 | src/requirements.txt |
| https://osv.dev/GHSA-h5c8-rqwp-cp95 | | | | | | |
| https://osv.dev/PYSEC-2026-1474 | 5.4 | PyPI | jinja2 | 3.1.2 | 3.1.4 | src/requirements.txt |
| https://osv.dev/GHSA-h75v-3vvj-5mfj | | | | | | |
| https://osv.dev/PYSEC-2026-1475 | 7.8 | PyPI | jinja2 | 3.1.2 | 3.1.5 | src/requirements.txt |
| https://osv.dev/GHSA-q2x7-8rv6-6q7h | | | | | | |
| https://osv.dev/PYSEC-2023-221 | 7.5 | PyPI | werkzeug | 2.2.2 | 2.3.8 | src/requirements.txt |
| https://osv.dev/GHSA-hrfv-mqp8-q5rw | | | | | | |
| https://osv.dev/PYSEC-2023-57 | 2.6 | PyPI | werkzeug | 2.2.2 | 2.2.3 | src/requirements.txt |
| https://osv.dev/GHSA-px8h-6qxv-m22q | | | | | | |
| https://osv.dev/PYSEC-2023-58 | 7.5 | PyPI | werkzeug | 2.2.2 | 2.2.3 | src/requirements.txt |
| https://osv.dev/GHSA-xg9f-g7g7-2323 | | | | | | |
| https://osv.dev/PYSEC-2026-2043 | 7.5 | PyPI | werkzeug | 2.2.2 | 3.0.3 | src/requirements.txt |
| https://osv.dev/GHSA-2g68-c3qc-8985 | | | | | | |
| https://osv.dev/PYSEC-2026-2044 | 6.3 | PyPI | werkzeug | 2.2.2 | 3.1.5 | src/requirements.txt |
| https://osv.dev/GHSA-87hc-h4r5-73f7 | | | | | | |
| https://osv.dev/PYSEC-2026-2045 | 6.3 | PyPI | werkzeug | 2.2.2 | 3.0.6 | src/requirements.txt |
| https://osv.dev/GHSA-f9vj-2wh5-fj8j | | | | | | |
| https://osv.dev/PYSEC-2026-2046 | 6.3 | PyPI | werkzeug | 2.2.2 | 3.1.4 | src/requirements.txt |
| https://osv.dev/GHSA-hgf8-39gv-g3f2 | | | | | | |
| https://osv.dev/PYSEC-2026-2320 | 6.3 | PyPI | werkzeug | 2.2.2 | 3.1.6 | src/requirements.txt |
| https://osv.dev/GHSA-29vq-49wr-vm6x | | | | | | |
| https://osv.dev/PYSEC-2026-3417 | 7.5 | PyPI | werkzeug | 2.2.2 | 3.0.6 | src/requirements.txt |
| https://osv.dev/GHSA-q34m-jh98-gwm2 | | | | | | |
| https://osv.dev/GHSA-g6x2-hccm-hh4m | 6.3 | PyPI | werkzeug | 2.2.2 | 3.1.9 | src/requirements.txt |
+-------------------------------------+------+-----------+----------+---------+---------------+----------------------+في المخرج أعلاه لاحظ التالي:
- VULN-06 وجدها بالكامل: سبع عشرة ثغرة منشورة في المكتبات الثلاث، ثنتان في Flask وخمس في Jinja2 وعشر في Werkzeug، ولكل منها الإصدار الذي يصلحها في العمود
FIXED VERSION. - اقرأ الملخص بحذر: السطر يقول ست حزم وأربع وثلاثون ثغرة، والملف فيه ثلاث حزم والجدول فيه سبع عشرة ثغرة مميزة، وهذا لأن الإصدار نفسه عد كل شيء مرتين، فلا تبن قراراً على الرقم في الملخص قبل أن تنظر إلى التفاصيل، والعدد الصحيح هنا هو الذي تراه في الجدول.
- ما فاته: كل شيء آخر، فهو لا يقرأ كودك ولا يرى ال Docker Image ولا حزم نظام التشغيل فيها.
الأداة الرابعة: Trivy لفحص ال Docker Image
وأخيراً نفحص ال Docker Image نفسها باستخدام Trivy (ترخيص Apache-2.0)، فنحفظها في ملف tar ثم نطلب منه الثغرات من مستوى HIGH وCRITICAL فقط، حتى يبقى المخرج مقروءاً:
docker save lab-dso-hub-app:demo -o app.tar
trivy image --input app.tar --scanners vuln --severity HIGH,CRITICALوالمخرج سوف يكون كما يلي، وقد حذفنا من جدول Debian سبعة وثلاثين صفاً من مستوى HIGH حتى لا يطول، وأبقينا الصفين الحرجين وجدول Python كاملاً:
2026-10-06T14:47:35+03:00 WARN This OS version is no longer supported by the distribution family="debian" version="10.13"
2026-10-06T14:47:35+03:00 WARN The vulnerability detection may be insufficient because security updates are not provided
app.tar (debian 10.13)
======================
Total: 42 (HIGH: 40, CRITICAL: 2)
┌──────────────┬────────────────┬──────────┬──────────────┬─────────────────────────┬────────────────────────┬──────────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├──────────────┼────────────────┼──────────┼──────────────┼─────────────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libdb5.3 │ CVE-2019-8457 │ CRITICAL │ will_not_fix │ 5.3.28+dfsg1-0.5 │ │ sqlite: heap out-of-bound read in function rtreenode() │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-8457 │
├──────────────┼────────────────┼──────────┼──────────────┼─────────────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤
│ libexpat1 │ CVE-2023-52425 │ HIGH │ fixed │ 2.2.6-2+deb10u6 │ 2.2.6-2+deb10u7 │ expat: parsing large tokens can trigger a denial of service │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-52425 │
│ … 37 more HIGH rows omitted here …
│ zlib1g │ CVE-2023-45853 │ CRITICAL │ will_not_fix │ 1:1.2.11.dfsg-1+deb10u2 │ │ zlib: integer overflow and resultant heap-based buffer │
│ │ │ │ │ │ │ overflow in zipOpenNewFileInZip4_6 │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-45853 │
└──────────────┴────────────────┴──────────┴──────────────┴─────────────────────────┴────────────────────────┴──────────────────────────────────────────────────────────────┘
Python (python-pkg)
===================
Total: 7 (HIGH: 7, CRITICAL: 0)
┌───────────────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐
│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │
├───────────────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ Flask (METADATA) │ CVE-2023-30861 │ HIGH │ fixed │ 2.2.2 │ 2.3.2, 2.2.5 │ flask: Possible disclosure of permanent session cookie due │
│ │ │ │ │ │ │ to missing Vary: Cookie... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-30861 │
├───────────────────────┼────────────────┤ │ │ ├───────────────┼─────────────────────────────────────────────────────────────┤
│ Werkzeug (METADATA) │ CVE-2023-25577 │ │ │ │ 2.2.3 │ python-werkzeug: high resource usage when parsing multipart │
│ │ │ │ │ │ │ form data with many fields... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-25577 │
│ ├────────────────┤ │ │ ├───────────────┼─────────────────────────────────────────────────────────────┤
│ │ CVE-2024-34069 │ │ │ │ 3.0.3 │ python-werkzeug: user may execute code on a developer's │
│ │ │ │ │ │ │ machine │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-34069 │
├───────────────────────┼────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ setuptools (METADATA) │ CVE-2022-40897 │ │ │ 57.5.0 │ 65.5.1 │ pypa-setuptools: Regular Expression Denial of Service │
│ │ │ │ │ │ │ (ReDoS) in package_index.py │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-40897 │
│ ├────────────────┤ │ │ ├───────────────┼─────────────────────────────────────────────────────────────┤
│ │ CVE-2024-6345 │ │ │ │ 70.0.0 │ pypa/setuptools: Remote code execution via download │
│ │ │ │ │ │ │ functions in the package_index module in... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-6345 │
│ ├────────────────┤ │ │ ├───────────────┼─────────────────────────────────────────────────────────────┤
│ │ CVE-2025-47273 │ │ │ │ 78.1.1 │ setuptools: Path Traversal Vulnerability in setuptools │
│ │ │ │ │ │ │ PackageIndex │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-47273 │
├───────────────────────┼────────────────┤ │ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤
│ wheel (METADATA) │ CVE-2026-24049 │ │ │ 0.40.0 │ 0.46.2 │ wheel: wheel: Privilege Escalation or Arbitrary Code │
│ │ │ │ │ │ │ Execution via malicious wheel file... │
│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2026-24049 │
└───────────────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘في المخرج أعلاه لاحظ التالي:
- VULN-07 وجد نصفها الآخر: التحذير
This OS version is no longer supportedيقول إن Debian 10 انتهى دعمه، وإن الفحص نفسه قد يكون ناقصاً لأن التوزيعة لم تعد تنشر إصلاحات. وفي حزم النظام 42 ثغرة من مستوى HIGH وCRITICAL، ومن بينها الثغرتان الحرجتان فيzlib1gوlibdb5.3وحالتهماwill_not_fix، أي أن التوزيعة لن تصلحهما، والحل الوحيد هو تغيير ال Base Image، وهذا موضوع مقال فحص الحاويات. - ما لم يكتبه أحد في أي ملف: ثغرات في
setuptoolsوwheel، وهي مكتبات جاءت مع ال Base Image، فلا تظهر فيrequirements.txtولم يرها OSV-Scanner. - VULN-06 مرة ثانية ولكن بشكل مختلف: وجد Flask وWerkzeug المثبتين داخل ال Image، لكن بعدد أقل، فهنا ثلاث ثغرات من مستوى HIGH فقط، ولو أزلنا الخيار
--severityلظهرت 29 ثغرة في حزم Python بكل المستويات، لأن كل أداة تعتمد على قاعدة بيانات مختلفة وتصنيف خطورة مختلف، فلا تتوقع أن تتطابق الأرقام بين أداتين. - ما فاته: لم يقل شيئاً عن تشغيل التطبيق بصلاحيات root، لأننا طلبنا فحص الثغرات فقط، وهذا يحتاج فحص الإعداد
trivy configعلى ملف Dockerfile، ولم ير أياً من ثغرات الكود.
مصفوفة النتائج Findings Matrix: من وجد ماذا؟
والآن نجمع النتائج في مكان واحد، والصورة التالية تبين كل ثغرة والأداة التي وجدتها:

| الثغرة | Gitleaks | Semgrep | OSV-Scanner | Trivy image |
|---|---|---|---|---|
| VULN-01 سران في الكود | التوكن فقط | لم يجدها | لم يجدها | لم نشغل فحص الأسرار فيه |
| VULN-02 حقن SQL | لم يجدها | وجدها | لم يجدها | لم يجدها |
| VULN-03 XSS منعكس | لم يجدها | وجدها | لم يجدها | لم يجدها |
| VULN-04 الترويسات الناقصة | لم يجدها | لم يجدها | لم يجدها | لم يجدها |
| VULN-05 إعادة التوجيه المفتوحة | لم يجدها | لم يجدها | لم يجدها | لم يجدها |
| VULN-06 المكتبات القديمة | لم يجدها | لم يجدها | وجدها: 17 ثغرة منشورة | وجدها داخل ال Image |
| VULN-07 ال Base Image وroot | لم يجدها | غياب USER فقط | لم يجدها | ال Base Image القديمة فقط |
ولاحظ في خانة Trivy من الصف الأول أن Trivy يستطيع فحص الأسرار أيضاً، لكننا طلبنا منه الثغرات فقط بالخيار --scanners vuln، وهذا يتكرر كثيراً في الواقع، فالأداة التي يمكنها أن تجد الشيء لا تجده إذا لم تطلب منها ذلك.
والخلاصة من هذه التجربة الصغيرة أن كل أداة أدت عملها جيداً في طبقتها، وفشلت تماماً خارجها، فلو اكتفيت ب SAST لنشرت مكتبات فيها سبع عشرة ثغرة منشورة، ولو اكتفيت ب SCA لنشرت حقن SQL يعرض جدول المستخدمين، ولو شغلت الأربع معاً لبقيت إعادة التوجيه المفتوحة والترويسات الناقصة، وهما أوضح شيء عندما تنظر إلى التطبيق من الخارج، وهذا عمل DAST الذي يشرحه مقال DAST على التطبيق نفسه. ولاحظ أيضاً أن مفتاح الجلسات فات أداة الأسرار، فلا تقم بإلغاء مراجعة الكود Code Review لأنك أضفت أداة، والسبب أن الأداة تجد الأنماط التي تعرفها فقط.
إزالة التجربة Cleanup
نحذف ما أنشأناه فقط بالاسم، دون أي أمر prune قد يحذف ما يخص مشاريع أخرى:
docker rm -f lab-dso-hub-app
docker image rm lab-dso-hub-app:demo
rm app.tarفرز النتائج Triage: من قائمة طويلة إلى قرار
أول ما يحدث عندما يشغل فريق هذه الأدوات على مشروع قديم هو أن تظهر مئات النتائج، فيشعر الجميع بالعجز، ثم يتجاهلونها. والحل ليس أن تصلح كل شيء الآن، وإنما أن تعرف ما الذي تصلحه أولاً، والصورة التالية تبين ترتيب الأسئلة:

درجة الخطورة CVSS
اختصار Common Vulnerability Scoring System، وهو المقياس الذي تراه في العمود CVSS في مخرج OSV-Scanner، رقم من صفر إلى عشرة تحسبه من خصائص الثغرة، مثل هل تستغل عبر الشبكة، وهل تحتاج تسجيل دخول، وما أثرها على السرية والسلامة والتوفر. والإصدار الحالي هو CVSS v4.0 الذي نشرته منظمة FIRST في نوفمبر 2023، ويقسم الدرجات إلى منخفضة Low من 0.1 إلى 3.9، ومتوسطة Medium من 4.0 إلى 6.9، وعالية High من 7.0 إلى 8.9، وحرجة Critical من 9.0 إلى 10.0.
وأهم ما يجب أن تعرفه عن CVSS هو ما يقوله دليل المستخدم نفسه، وهو أن الدرجة الأساسية Base تقيس خطورة الثغرة ولا يجب استخدامها وحدها لتقدير الخطر Risk، فالثغرة الحرجة في دالة لا يستدعيها تطبيقك أقل خطراً عليك من ثغرة متوسطة في صفحة تسجيل الدخول. لذلك يضيف الإصدار الرابع مجموعة التهديد Threat ومجموعة البيئة Environmental، ويسمي الدرجة بحسب ما استخدم منها، مثل CVSS-B للأساسية وحدها وCVSS-BTE للثلاث معاً.
احتمال الاستغلال EPSS
اختصار Exploit Prediction Scoring System، وهو نموذج تعلم آلي من FIRST أيضاً يقدر احتمال أن تستغل ثغرة منشورة في الواقع خلال الثلاثين يوماً القادمة، ويحدث الأرقام كل يوم لكل CVE، والبيانات مجانية عبر ملف CSV وAPI. فإذا كان CVSS يجيب عن «كم هي سيئة لو استغلت؟» فإن EPSS يجيب عن «ما احتمال أن يستغلها أحد قريباً؟». ولنأخذ مثالاً من مكتبات تطبيقنا، فقد سألنا ال API عن ثلاث منها:
curl -s "https://api.first.org/data/v1/epss?cve=CVE-2023-30861,CVE-2024-34069,CVE-2024-22195"والمخرج سوف يكون كما يلي:
{"status":"OK","status-code":200,"version":"1.0","access":"public","total":3,"offset":0,"limit":100,"data":[{"cve":"CVE-2024-34069","epss":"0.033970000","percentile":"0.884580000","date":"2026-10-05"},{"cve":"CVE-2024-22195","epss":"0.008920000","percentile":"0.580420000","date":"2026-10-05"},{"cve":"CVE-2023-30861","epss":"0.012610000","percentile":"0.686130000","date":"2026-10-05"}]}في المخرج أعلاه لاحظ أن ثغرة Werkzeug رقم CVE-2024-34069، وهي في أداة التنقيح Debugger، احتمال استغلالها نحو 3.4% خلال شهر، وهو رقم يبدو صغيراً، لكن العمود percentile يقول إنه أعلى من نحو 88% من كل الثغرات المنشورة، أما ثغرة Flask رقم CVE-2023-30861 فاحتمالها نحو 1.3%. والأرقام تتغير كل يوم، لذلك تسأل عنها وقت الفرز ولا تحفظها.
قائمة الثغرات المستغلة فعلاً KEV
قائمة Known Exploited Vulnerabilities تنشرها وكالة الأمن السيبراني الأمريكية CISA، وهي قائمة بالثغرات التي ثبت أنها استغلت في الواقع، وكان فيها 1734 ثغرة في نسختها الصادرة في 4 أكتوبر 2026، وتنشر بصيغة JSON وCSV. وإذا كان EPSS تقديراً للمستقبل فإن KEV حقيقة عن الحاضر، لذلك نضعها في المخطط قبل EPSS، فالثغرة الموجودة في KEV وفي تطبيقك تصلح خلال أيام، مهما كانت درجة CVSS. وقد بحثنا في القائمة عن Flask وWerkzeug وJinja2 فلم نجد أياً منها، وهذا متوقع، فأغلب ما تجده الأدوات في تطبيقك لن يكون في KEV، وهذا ما يجعلها مفيدة للترتيب.
الإنذارات الخاطئة False Positives
الإنذار الخاطئ هو نتيجة تقول الأداة إنها ثغرة وهي ليست كذلك في سياقك، مثل نتيجة 0.0.0.0 في تجربتنا. والطريقة الخاطئة في التعامل معه هي تعطيل القاعدة كلها، أو إضافة المجلد كله إلى قائمة الاستثناء، والسبب أنك بذلك تخفي الثغرات الحقيقية التي تجدها القاعدة نفسها في أماكن أخرى. والطريقة الصحيحة هي أن تتجاهل النتيجة الواحدة في مكانها، بتعليق في السطر أو بملف استثناء، ومعها السبب، وفي الأدوات التي تسمح بذلك تاريخ انتهاء، حتى تعود النتيجة للمراجعة إذا تغير السياق. والإنذار الخاطئ له تكلفة حقيقية، فكل نتيجة خاطئة يراها المطور تقلل ثقته في الأداة، وبعد عشر نتائج خاطئة سوف يتجاهل الحادية عشرة وهي حقيقية.
الخط الأساسي Baseline: لا تصلح الماضي كله اليوم
عندما تضيف أداة إلى مشروع عمره خمس سنوات فسوف تجد مئات النتائج القديمة، فإذا جعلت الخط يفشل بسببها توقف الفريق كله. والحل المعروف هو الخط الأساسي Baseline، أي أن تسجل النتائج الموجودة اليوم كدين معروف له خطة منفصلة، وتجعل الأداة تفشل فقط على النتائج الجديدة التي أدخلها طلب الدمج الحالي. وبهذا الشكل نكون قد أوقفنا زيادة المشكلة من اليوم الأول، بينما يتناقص الدين القديم بالتدريج، وأغلب الأدوات تدعم ذلك، إما بمقارنة النتائج مع فرع main، أو بملف يحفظ بصمات النتائج القديمة مثل Fingerprint الذي رأيته في مخرج Gitleaks.
مواعيد الإصلاح SLA
الفرز لا معنى له إذا لم ينته بموعد، لذلك يكتب الفريق سياسة بسيطة تربط الأولوية بمدة الإصلاح، وتراجع شهرياً. ولا توجد أرقام قياسية يجب أن تلتزم بها، فالمثال التالي نقطة بداية معقولة لفريق صغير، وعليك أن تعدله بحسب نوع تطبيقك وما يكشفه للإنترنت:
| الحالة | مثال لموعد الإصلاح |
|---|---|
| في قائمة KEV ويصل إليها المهاجم، أو سر مسرب | خلال أيام، والسر يدور فوراً |
| Critical، أو EPSS مرتفع | خلال أسبوع أو أسبوعين |
| High | خلال شهر |
| Medium | خلال ثلاثة أشهر، أو مع التحديث الدوري |
| Low | مع التحديث الدوري، أو قرار مكتوب بقبول الخطر |
الناس والثقافة Culture قبل الأدوات
سفير الأمن Security Champion في كل فريق
الأدوات تجد المشاكل، لكن أحداً يجب أن يقرأ النتائج ويقرر ويعدل القواعد ويجيب عن سؤال المطور «هل هذه النتيجة حقيقية؟». وفي أغلب الفرق لا يوجد مختص أمن لكل فريق، لذلك تستخدم الشركات فكرة سفير الأمن Security Champion، وهو مطور من الفريق نفسه يهتم بالأمن، يأخذ وقتاً محدداً من أسبوعه لهذا الدور، ويكون حلقة الوصل مع فريق الأمن. ولدى OWASP مشروع كامل لهذا هو OWASP Security Champions Guide. وأهم ما في الفكرة أن يكون الوقت معترفاً به في خطة الفريق، والسبب أن الدور الذي يضاف فوق العمل الكامل دون وقت ينتهي بعد شهرين.
هل تمنع البوابة Gate الدمج أم تنبه فقط؟
كل فحص في الخط إما أن يمنع الدمج إذا وجد شيئاً Blocking، أو يكتب تعليقاً وينجح Warning. والطريقة الخاطئة الأولى أن تجعل كل شيء يمنع من اليوم الأول، فيمتلئ الخط بالفشل بسبب نتائج قديمة وإنذارات خاطئة، فيضغط أحدهم على زر التجاوز، أو يعطل الفحص «مؤقتاً» إلى الأبد. والطريقة الخاطئة الثانية أن يبقى كل شيء تنبيهاً إلى الأبد، فلا يقرأ التنبيهات أحد. والحل الأنسب أن تمنع فقط ما تثق فيه وما لا خلاف عليه، مثل الأسرار، والثغرات الجديدة من مستوى Critical أو الموجودة في KEV ولها إصلاح، والنتائج الجديدة من قواعد SAST التي جربتها وتثق في دقتها، وأن يبقى الباقي تنبيهاً يراجعه سفير الأمن، ثم تنقل القواعد من التنبيه إلى المنع كلما أثبتت دقتها.
خطة إدخال الفحوص على مراحل Staged Adoption
إذا كان فريقك لا يشغل أي فحص اليوم، فلا تقم بإضافة سبعة فحوص في أسبوع واحد، والسبب أن الفريق سوف يرى مئات النتائج دفعة واحدة ويحكم على الفكرة كلها بالفشل. والصورة التالية تبين خطة على أربع مراحل:

- الأسبوع الأول، الأسرار والرؤية: فحص الأسرار يمنع من اليوم الأول، لأن أحداً لن يدافع عن مفتاح في الكود، وفحص الاعتماديات يعمل للتقرير فقط، والهدف أن تعرف حجم ما لديك قبل أن تقرر.
- الأسابيع من الثاني إلى الرابع، الكود والمكتبات: SAST على طلبات الدمج مع خط أساسي، فيمنع الجديد فقط، وفحص الاعتماديات يمنع الثغرات الجديدة من مستوى Critical أو في KEV، ومعه بوت يفتح طلبات دمج لتحديث المكتبات حتى لا يتراكم الدين.
- الشهر الثاني، ال Image: فحص ال Docker Image عند البناء، وقواعد Dockerfile وIaC، وإعادة فحص ليلية لل Images التي تعمل في الإنتاج.
- الشهر الثالث وما بعده، التطبيق أثناء التشغيل: DAST على بيئة staging أو المعاينة، ولوحة واحدة تجمع النتائج من كل الأدوات، ومواعيد إصلاح مكتوبة، ونمذجة التهديدات لكل ميزة كبيرة جديدة.
وقس التقدم بأرقام قليلة واضحة، مثل عدد النتائج المفتوحة من مستوى Critical وHigh، ومتوسط الزمن من اكتشاف الثغرة إلى إصلاحها، وعدد الأسرار التي وصلت إلى المستودع، ولا تقس عدد النتائج التي وجدتها الأدوات، لأن هذا الرقم يرتفع كلما أضفت أداة ولا يقول شيئاً عن أمان التطبيق.
مسار القراءة Reading Path في السلسلة
هذا المقال أعطاك الصورة الكاملة، والمقالات التالية تأخذ كل فحص وتشرحه من الداخل، مع أدواته ونقاط القوة ونقاط الضعف في كل أداة، وتجربة على التطبيق نفسه، وطريقة تشغيله في خط CI، بهذا الترتيب:
دليلSecrets Scanning: كيف تكتشف الأسرار المسربة في الكودكيف يخرج المفتاح من جهازك إلى تاريخ Git وخط CI، وكيف تكتشفه قبل غيرك، وماذا تفعل في الساعة الأولى بعد تسربه.
دليلSAST: كيف يجد تحليل الكود الثابت الثغرات قبل التشغيلكيف تقرأ أداة SAST الكود بحثاً عن الثغرات، وما الذي لا تراه أبداً، وكيف تمنع الإنذارات الخاطئة من أن تفقد الفريق ثقته بها.
دليلSCA: كيف تجد الثغرات المعروفة في مكتبات مشروعككيف تجد أداة فحص الاعتماديات SCA المكتبات المصابة في مشروعك، ولماذا تختلف نتائج الأدوات، وكيف تبقي مكتباتك محدثة.
دليلفحص ال Docker Images وملفات البنية ككود IaCماذا يرى فاحص الثغرات داخل ال Docker Image وما الذي يفوته، وكيف تختار Base Image أصغر وتجعل الفحص بوابة في خط CI.
دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.
دليلخط CI/CD آمن من البداية للنهاية على Gitea Actionsكيف تجمع فحوص الأمن في خط واحد لا يستطيع طلب الدمج تجاوزه، ثم ترسل إلى الإنتاج نفس ال Docker Image الموقعة التي فحصتها.وقبلها أو بجانبها، مقالان يكملان الصورة، الأول عن التصميم قبل الكود، والثاني عن المكتبات وال Images وخط CI نفسه كهدف للمهاجم:
دليلنمذجة التهديدات Threat Modeling للمبتدئينكيف تسأل ما الذي قد يسوء في تصميم تطبيقك قبل أن تكتب الكود، وتقرر لكل تهديد هل تصلحه أو تقبله، مع قالب جاهز للنسخ.
دليلأمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئينكل ما بين كود غيرك وسيرفرك قد يحمل كود المهاجم، وهنا تعرف التهديدات الحقيقية بحوادث موثقة والدفاع المناسب لكل حلقة.وإذا كنت لم تقرأ بعد عن الاختبارات وخط CI على سيرفرك، فهذه المقالات هي الأساس الذي تبنى عليه السلسلة:
دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.
دليلاختبارات يكتبها الذكاء الاصطناعي: متى تثق بها؟أين يساعدك الذكاء الاصطناعي في كتابة الاختبارات وأين يخدعك، ولماذا يبقى قرار ما هو الصحيح بيد الإنسان.
دليلاختبار الأحمال والضغط Load Testing للمبتدئينلماذا يعمل تطبيقك مع خمسة مستخدمين وينهار مع خمسمئة، وكيف تكتشف ذلك وتصلحه قبل يوم الإطلاق.
دليلGitea Actions: تشغيل CI/CD وبناء Docker Images على سيرفركاجعل سيرفرك يختبر الكود ويبني Docker Images تلقائياً مع كل push، دون أن يخرج كودك أو أسرارك إلى خدمة CI خارجية.
دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.
دليلTrunk-Based Development: فروع قصيرة ودمج يوميلماذا تنفجر الفروع الطويلة يوم الإصدار، وكيف يدمج الفريق عمله في main كل يوم بأمان مع CI وFeature Flags.أخطاء شائعة Common Mistakes
- شراء أداة واحدة واعتبار المشكلة محلولة: رأيت في التجربة أن كل أداة ترى طبقة واحدة، وأن ثغرتين لم تجدهما أي أداة من الأربع.
- تشغيل كل الفحوص بوضع المنع من اليوم الأول: ينتهي بتعطيلها، فابدأ بالأسرار، ثم امنع الجديد فقط مع خط أساسي.
- الترتيب بدرجة CVSS وحدها: الدرجة تقيس الخطورة وليس الخطر، فأضف إليها KEV وEPSS وهل يصل المهاجم إلى الكود المصاب.
- تعطيل قاعدة كاملة بسبب إنذار خاطئ واحد: تجاهل النتيجة الواحدة في مكانها مع السبب.
- حذف السر المسرب في commit جديد ونسيانه: السر بقي في التاريخ، فدوره Rotate أولاً، ثم نظف التاريخ.
- فحص ال Image مرة واحدة عند البناء فقط: الحزم لا تتغير، لكن الثغرات المنشورة فيها تزيد كل يوم، فأعد الفحص دورياً لما يعمل في الإنتاج.
- تشغيل DAST على الإنتاج أو على موقع لا تملكه: مكانه بيئة المعاينة أو staging، وبإذن مكتوب إن لم يكن الموقع لك.
- نسيان أن خط CI نفسه هدف: الخط الذي يملك مفاتيح النشر ويشغل كوداً من طلبات الدمج يحتاج حماية، وهذا موضوع مقال سلسلة التوريد والمقال الأخير في السلسلة.
- إلقاء النتائج على المطورين دون شرح: نتيجة بلا سياق ولا طريقة إصلاح تتحول إلى ضجيج، وهنا دور سفير الأمن.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- الأمن الذي يأتي في نهاية المشروع يكتشف المشاكل متأخراً ومرة واحدة، أما DevSecOps فيشغل الفحوص آلياً مع كل تغيير، ويجعل الفريق الذي يكتب الكود مسؤولاً عن أمانه.
- كل فحص يرى طبقة واحدة: الأسرار وSAST يقرآن كودك، وSCA يقرأ المكتبات، وفحص الحاويات يقرأ ال Image، وDAST يرى التطبيق من الخارج، واختبار الاختراق ونمذجة التهديدات يغطيان ما لا تراه الأدوات.
- في التجربة وجدت أربع أدوات خمس ثغرات من سبع بشكل كامل أو جزئي، ولم تجد أي منها إعادة التوجيه المفتوحة ولا الترويسات الناقصة، وفات أداة الأسرار مفتاح الجلسات.
- كل فحص يعمل في أبكر مكان يستطيع أن يرى فيه ما يبحث عنه، وطلب الدمج هو أهم هذه الأماكن.
- رتب النتائج بالسؤال عن حقيقتها، ثم هل يصل إليها المهاجم، ثم KEV، ثم EPSS، ثم CVSS، واجعل لكل أولوية موعداً مكتوباً.
- ابدأ صغيراً، وامنع الجديد فقط، واجعل في كل فريق سفيراً للأمن له وقت معترف به.
وفي المقالات القادمة بإذن الله سوف نأخذ التطبيق نفسه ونصلحه طبقة بعد طبقة، بدءاً من الأسرار المسربة.
سجل التحديثات
- أكتوبر 2026: كتابة المقال، وإنشاء التطبيق التجريبي للسلسلة، وتنفيذ التجربة بأدوات Gitleaks 8.30.1 وSemgrep 1.178.0 وOSV-Scanner 2.6.0 وTrivy 0.75.0 في Docker، والتحقق من CVSS v4.0 وEPSS وقائمة KEV من مصادرها.