📚 محتوى دفاعي بحت: فئة أضيفت حديثًا لقائمة OWASP Top 10 (مراجعة 2025) — الدرس السابع عن OWASP يشير لها.
المشكلة: الخطأ نفسه ثغرة
معظم الدروس السابقة تعاملت مع الثغرات كخلل في المسار الطبيعي للكود (حقن، وصول غير مصرّح، إلخ). لكن جزءًا كبيرًا من الاختراقات الحقيقية يحدث في المسار غير الطبيعي: ماذا يفعل تطبيقك تحديدًا حين يفشل اتصال قاعدة البيانات؟ حين تنفد الذاكرة؟ حين تفشل عملية تشفير؟ حين يصطدم طلبان بنفس معالج الخطأ في نفس اللحظة؟
لو لم تُصمَّم هذه الحالات الاستثنائية بعناية، قد تتجاوز التحقّقات الأمنية بصمت دون أي خلل ظاهر في الكود "السليم".
سيناريو 1: كشف تفاصيل داخلية عبر رسالة الخطأ
// ❌ خطر: رسالة الخطأ الخام تصل للمستخدم كما هي
app.use((err, req, res, next) => {
res.status(500).send(err.stack); // يكشف مسارات ملفات، نسخة المكتبة، أحيانًا استعلام SQL كاملًا
});
رسالة خطأ تفصيلية (stack trace، اسم جدول، إصدار مكتبة) لا تخترق شيئًا بنفسها، لكنها استطلاع مجاني لمهاجم يجهّز محاولة حقن أو استغلال ثغرة معروفة بإصدار محدّد.
// ✅ آمن: رسالة عامة للمستخدم، التفاصيل الكاملة تُسجَّل داخليًّا فقط
app.use((err, req, res, next) => {
logger.error(err); // التفاصيل الكاملة في نظام التسجيل الداخلي (الدرس عن التسجيل والمراقبة)
res.status(500).json({ message: "حدث خطأ غير متوقّع، حاول لاحقًا." });
});
سيناريو 2: الفشل المفتوح (Fail Open)
الأخطر: كود يتعامل مع فشل فحص أمني بالسماح بدل الرفض عند حدوث استثناء غير متوقّع.
// ❌ خطر جدًّا: أي استثناء في فحص الصلاحية يُعامَل كـ "مسموح"
try {
if (!userHasPermission(user, resource)) return forbidden();
} catch {
// نسيان معالجة صريحة = المتابعة كأن الفحص نجح!
}
proceedWithAction();
لو رمى userHasPermission استثناءً غير متوقّع (مثلًا خدمة الصلاحيات لم تستجب)، الكود أعلاه يتجاهل الفشل ويكمل التنفيذ — تحقّق أمني كامل تحوّل لعدم فعل شيء عمليًّا.
// ✅ آمن: أي استثناء = رفض صريح (fail closed)، لا استمرار افتراضي
try {
if (!userHasPermission(user, resource)) return forbidden();
} catch (err) {
logger.error(err);
return forbidden(); // الفشل بحد ذاته سبب كافٍ للرفض، لا للسماح
}
proceedWithAction();
⚠️ مبدأ الفشل الآمن (Fail Closed): أي فحص أمني (مصادقة، صلاحيات، تشفير) يجب أن يرفض بشكل افتراضي عند حدوث أي خطأ غير متوقّع أثناء الفحص نفسه — لا أن يفترض النجاح. "لم أستطع التحقّق" يجب أن تُعامَل مثل "غير مسموح"، أبدًا مثل "مسموح".
سيناريو 3: استنزاف الموارد (Resource Exhaustion)
// ❌ خطر: عند فشل معالجة الملف، القفل/المورد المؤقّت لا يُحرَّر أبدًا
function handleUpload(file) {
const lock = acquireLock(file.id);
processFile(file); // لو رمت استثناءً، التنفيذ يتوقف هنا والقفل يبقى محجوزًا للأبد
releaseLock(lock);
}
استثناء غير متوقّع أثناء المعالجة يمنع تنفيذ سطر التحرير (releaseLock) — تكرار العملية عدّة مرات كافٍ لاستنزاف كل الموارد المتاحة وحجب الخدمة عن مستخدمين شرعيين، دون أن يستغلّ المهاجم أي "ثغرة" تقليدية.
// ✅ آمن: التحرير مضمون بغضّ النظر عن نجاح المعالجة أو فشلها
function handleUpload(file) {
const lock = acquireLock(file.id);
try {
processFile(file);
} finally {
releaseLock(lock); // ينفَّذ دائمًا، حتى مع استثناء
}
}
سيناريو 4: تلف المعاملات متعدّدة الخطوات
عملية تحويل رصيد من حساب لآخر تتكوّن عادة من عدّة خطوات (خصم من الأول، إضافة للثاني، تسجيل العملية). لو فشلت خطوة في المنتصف دون تراجع كامل (rollback) عن الخطوات السابقة، قد ينتهي الأمر بخصم دون إضافة، أو تنفيذ العملية مرّتين عند إعادة المحاولة.
💡 لهذا السبب بالضبط تُستخدم المعاملات الذرّية (
transactions) في قواعد البيانات: إمّا تنجح كل الخطوات معًا، أو تتراجع كلها معًا — لا حالة وسطى أبدًا.
قائمة تحقّق سريعة
- رسائل خطأ عامة للمستخدم، والتفاصيل الكاملة في نظام تسجيل داخلي فقط.
- أي استثناء غير متوقّع في فحص أمني = رفض (
fail closed)، لا استمرار افتراضي. - تحرير الموارد (أقفال، اتصالات) داخل
finallyلا بعد سطر قد لا يُنفَّذ. - عمليات متعدّدة الخطوات ضمن معاملة ذرّية (
transaction) تدعم التراجع الكامل عند الفشل. - معالج أخطاء عام (
global error handler) كشبكة أمان أخيرة، لا اعتماد كامل على معالجة يدوية بكل مسار.
🎯 التالي: خلاصة المسار.