تجنب حقن SQL وأفضل الممارسات
تجنب حقن SQL وأفضل الممارسات
احتلّ حقن SQL المرتبة الأولى أو المراتب القريبة منها في قائمة OWASP Top 10 لأكثر من عقدين — ليس لأنه يصعب التصدي له، بل لأن المطورين يستمرون في دمج مدخلات المستخدم مع سلاسل SQL. يُرسّخ هذا الدرس طبقات الدفاع التي يجب أن يمتلكها أي تطبيق JDBC احترافي: التحديد بمعاملات كضابط أساسي، والتحقق من صحة المدخلات كدفاع ثانوي، وحسابات قاعدة البيانات محدودة الصلاحيات، وجملة من أنظمة البرمجة التي تُقضي على فئات كاملة من الثغرات.
ما هو حقن SQL فعليًا
تقع هجمة الحقن حين يُدمج نص يُوفره المستخدم مباشرةً في سلسلة SQL فتُنفّذ قاعدة البيانات الجزء الذي يتحكم فيه المهاجم بوصفه جزءًا من الاستعلام. المثال الكلاسيكي — نموذج تسجيل الدخول:
إذا أدخل أحد المهاجمين admin' -- بوصفها اسم مستخدم، يصبح SQL المُنتج:
الشرطتان المزدوجتان تُعلّقان التحقق من كلمة المرور. تُعاد كل صف تكون فيه username = 'admin' متجاوزًا المصادقة كليًا. الحمولات الأشد تدميرًا قادرة على تنفيذ DROP TABLE، أو تسريب جميع الصفوف، أو استدعاء إجراءات مخزّنة تُعدّل ملفات على مستوى نظام التشغيل.
الدفاع الأساسي: التحديد بمعاملات مع PreparedStatement
PreparedStatement هو خط الأساس الذي لا يمكن التنازل عنه. عندما تُمرّر معاملًا عبر مُعيِّن مكتوب، يُرسل مشغّل JDBC قالب SQL والقيمة على شكل رسالتين شبكيتين منفصلتين. لا تحلّل قاعدة البيانات القيمة أبدًا باعتبارها SQL — بل تتعامل معها باعتبارها سلسلة حرفية أو رقمًا أو كتلة بيانات ثنائية.
الجمل الديناميكية: قوائم IN والأعمدة المُرتَّبة
يُغطي التحديد بمعاملات القيم القياسية بسهولة. ثمة نمطان يُوقعان المطورين الذين فهموا الأساسيات بالفعل:
قوائم IN ذات الطول المتغير. لا يمكن ربط قائمة كاملة بـ ? واحدة. ابنِ سلسلة العناصر النائبة برمجيًا، ثم اربط كل عنصر منفردًا:
أعمدة الترتيب. لا يمكن تمرير أسماء الأعمدة كمعاملات ربط — تحتاج إليها قاعدة البيانات في وقت التحليل. استخدم قائمة مسموحة:
الدفاع الثانوي: التحقق من صحة المدخلات
يُوقف التحديد بمعاملات الحقن؛ أما التحقق فيمنع وصول البيانات السيئة إلى قاعدة البيانات أصلًا. كلاهما مكمّل للآخر لا بديل عنه — طبّق الاثنين معًا.
تحقق مبكرًا عند الحدود التي تدخل منها المدخلات إلى نظامك (سيرفلت أو نقطة نهاية REST أو دالة خدمة). لكل حقل اطرح ثلاثة أسئلة:
- النوع: هل هذا رقم / تاريخ / UUID فعلًا؟ حاول التحليل وارفض عند الإخفاق قبل لمس DAO.
- النطاق / الطول: هل يُغذَّى عمود
VARCHAR(150)بسلسلة من 10,000 حرف؟ ارفضها؛ لا تعتمد على قاعدة البيانات لاقتطاعها بصمت. - التنسيق: لعناوين البريد الإلكتروني وأرقام الهواتف وعناوين URL قواعد بنيوية. طبّقها بتعبير نمطي أو مكتبة مخصصة — لا تستخدم أبدًا جملة SQL LIKE.
حسابات قاعدة البيانات محدودة الصلاحيات
حتى الكود المُحدَّد تمامًا بمعاملات لا يستطيع منع الضرر إذا كان حساب قاعدة البيانات الخاص بالتطبيق يمتلك حقوقًا غير مقيّدة. اتّبع مبدأ أدنى الصلاحيات: امنح مستخدم التطبيق فقط الأذونات التي يحتاجها فعليًا.
بهذا الإعداد، حتى لو استغل مهاجم خللًا منطقيًا ونفّذ SQL عشوائيًا من خلال حساب التطبيق، لا يستطيع تنفيذ DROP TABLE، ولا قراءة /etc/passwd عبر LOAD DATA INFILE، ولا الوصول إلى جداول خارج المجموعة المسموح بها.
أفضل ممارسات إضافية
لا تخزّن كلمات المرور كنص عادي أبدًا. خزّن فقط هاش كلمة المرور المنتج بخوارزمية بطيئة ومُملَّحة. BCrypt من Spring Security أو java.security.MessageDigest مع KDF مناسب (PBKDF2) هما الخياران المعياريان. هاش SHA-256 الخام بدون ملح غير مقبول.
تجنب SELECT *. سمّ دائمًا الأعمدة التي تحتاجها. الاستعلامات ذات العلامة النجمية تسحب بيانات ربما لا تنوي الكشف عنها، وتتعطل عند تغيير ترتيب الأعمدة، وتجعل كود التعيين أصعب في المراجعة.
سجّل الاستعلامات دون المعاملات. إذا احتجت تسجيل SQL لأغراض التصحيح، سجّل القالب فقط — لا القيم المرتبطة أبدًا. سطر سجل يحتوي على WHERE password_hash = 'bcrypt$...' يُسرّب بيانات حساسة لكل من يملك صلاحية الوصول إلى السجلات.
استخدم قيود المخطط كخط دفاع أخير. قيود NOT NULL وUNIQUE وCHECK والمفاتيح الأجنبية تُمسك بمشاكل تكامل البيانات التي تفوت التحقق على مستوى التطبيق. ليست بديلًا عن التحقق على مستوى التطبيق بل دعامة موثوقة.
قائمة مراجعة: الوصول الآمن للبيانات
- كل استعلام يلمس مدخلات المستخدم يستخدم
PreparedStatementمع مُعيِّنات مكتوبة. - أسماء الأعمدة والجداول الديناميكية تُتحقق من صحتها بقائمة مسموحة صريحة قبل الدمج.
- جميع استعلامات قوائم IN تبني عناصر
?النائبة برمجيًا وتربط كل قيمة منفردة. - التحقق من صحة المدخلات (النوع والطول والنطاق والتنسيق) يجري عند حدود الخدمة قبل أي استدعاء DAO.
- حساب قاعدة البيانات الخاص بالتطبيق يحتفظ فقط بالصلاحيات الضرورية الدنيا.
- كلمات المرور تُخزَّن كهاش، لا كنص عادي.
- مخرجات السجل تحتوي على قوالب SQL، لا على قيم المعاملات المرتبطة.
- قيود المخطط (
NOT NULLوUNIQUEوCHECK) تدعم التحقق على مستوى التطبيق.
الخلاصة
يُمنع حقن SQL على المستوى المعماري باستخدام PreparedStatement لكل استعلام يتضمن بيانات خارجية. التحديد بمعاملات ليس خيارًا أسلوبيًا — بل هو الضمان الهيكلي بأن مدخلات المستخدم لا تستطيع أبدًا تغيير دلالات الاستعلام. أضف فوق ذلك: تحقق من المدخلات مبكرًا عند حدود الخدمة، طبّق حسابات قاعدة البيانات محدودة الصلاحيات، سمّ أعمدتك صراحةً، ودع قيود المخطط تعمل كدعامة. كل طبقة هشة منفردة؛ معًا تُشكّل استراتيجية وصول للبيانات متينة ومعمّقة في الدفاع تصمد أمام ظروف الهجوم الحقيقي.