كنت أعتقد أن كتابة طبقة المصادقة الخاصة بي هي وسام شرف. إذا كنت تفهم الـ JWTs وتستطيع تجزئة (hash) كلمة المرور، فما مدى صعوبة الأمر؟ قمت بإعداد خلفية Node، وأصدرت الرموز (tokens)، واعتبرت الأمر منتهياً. الكود كان يعمل، والاختبارات نجحت. ثم بدأت أقرأ عن كيفية حدوث الهجمات الحقيقية، وانهار كل شيء تحت قدمي. كل فصل عن الحقن (injection)، والتعداد (enumeration)، وتسريبات القنوات الجانبية (side-channel leaks) جعلني أعود إلى المحرر بشعور من الإحباط. لم يكن نظام المصادقة الخاص بي يحتوي على ثغرات فحسب؛ بل كان يحتوي على أربعة أبواب مفتوحة على مصراعيها قمت بتركيبها بنفسي. إليكم ما وجدته، وما قمت بتغييره بالضبط.

SQL Injection عبر دمج النصوص (String Concatenation)

كانت الثغرة الأولى هي أقدم خدعة في هذا المجال. كنت آخذ مدخلات المستخدم وأضعها مباشرة في نصوص SQL. في مسار تسجيل الدخول (login route)، كنت آخذ البريد الإلكتروني من جسم الطلب (request body) وأدمجه في استعلام مثل SELECT * FROM users WHERE email = '${email}'. بدا الأمر غير ضار لأنني كنت أتحكم في الواجهة الأمامية (frontend). هذا افتراض خطير؛ فالمهاجم لا يحتاج إلى واجهتك الأمامية. يمكن لجسم POST واحد مصمم بعناية أن يحول عملية التحقق من تسجيل الدخول هذه إلى خرق للبيانات أو مسح لقاعدة البيانات بالكامل. أرسل حمولة (payload) مثل ' OR '1'='1 أو ما هو أسوأ، استعلاماً متراكباً (stacked query) يحذف الجداول، وإذا تم تنفيذ النص كما هو، فستضيع بياناتك. لم أمنح قاعدة البيانات أي وسيلة للتمييز بين الكود الخاص بي وبيانات المهاجم.

لم يكن الحل هو المزيد من التحقق من المدخلات أو الهروب من النصوص (escaping strings) يدوياً. الحل الحقيقي كان الاستعلامات ذات المعاملات (parameterized queries). انتقلت إلى استخدام node-postgres وبدأت في استخدام العلامات النائبة (placeholders) مثل $1. يصبح الاستعلام قالباً: SELECT * FROM users WHERE email = $1. يرسل برنامج التشغيل (driver) استعلام SQL والقيم عبر قنوات منفصلة، وتتعامل قاعدة البيانات مع المدخلات كبيانات فقط، بغض النظر عن الأحرف التي تحتوي عليها. هذا التغيير الواحد يغلق فئة كاملة من هجمات الحقن. إنه أبسط في القراءة، وأسهل في الصيانة، ويزيل عبء كونك خبيراً في التعبيرات النمطية (regex) في كل مرة تكتب فيها جملة WHERE.

تعداد البريد الإلكتروني عبر رسائل الخطأ

بدا خطئي الثاني وكأنه تجربة مستخدم (UX) جيدة. عندما كان المستخدم يكتب بريداً إلكترونياً خاطئاً، كنت أُرجع User not found. وعندما يكتب البريد الصحيح ولكن كلمة المرور خاطئة، كنت أُرجع Incorrect password. بدا الأمر مفيداً، لكنه كان أيضاً أداة استطلاع للمهاجمين. يمكن لسكربتات التعداد (enumeration scripts) أن تنهال على نقطة نهاية تسجيل الدخول (login endpoint) بآلاف العناوين البريدية. إذا تغير جسم الاستجابة أو رمز الحالة (status code) بناءً على وجود الحساب من عدمه، فيمكن للسكربت بناء قائمة مؤكدة لمستخدميك. تصبح هذه القائمة أساساً لهجمات حشو الاعتمادات (credential stuffing)، والتصيد الاحتيالي المستهدف (targeted phishing)، ومحاولات القوة الغاشمة (brute-force) الإضافية.

كان عليّ أن أتقبل أن سهولة الاستخدام يجب أن تخسر أحياناً لصالح الأمان. قمت بتغيير كل مسار لتسجيل دخول فاشل ليعيد نفس السلسلة النصية تماماً: Invalid credentials. لا تلميحات، ولا منطق تفرعي (branching logic) في استجابة الخطأ. سواء كان البريد الإلكتروني مفقوداً، أو كلمة المرور خاطئة، أو الحساب مغلقاً، يظل النص متطابقاً. ينطبق هذا أيضاً على عمليات التسجيل وإعادة تعيين كلمة المرور؛ لا تكشف عما إذا كان العنوان موجوداً بالفعل في نظامك. رسالة عامة واحدة تزيل تسريباً للمعلومات يعتمد عليه المهاجمون.

هجمات التوقيت (Timing Attacks) في مقارنة كلمات المرور

كانت الثغرة الثالثة غير مرئية. كنت