PreparedStatement والحماية من حقن SQL
PreparedStatement والحماية من حقن SQL
في الدرس السابق أرسلتَ نصوص SQL مبنيّة بتسلسل السلاسل النصية. هذا النهج يعمل — حتى يقدّم أحدهم مدخلات خبيثة. يتناول هذا الدرس PreparedStatement، الآلية المعيارية في JDBC للاستعلامات ذات المعاملات، ويشرح بدقة لماذا المعاملات تُقضي على حقن SQL تمامًا لا تُخفّفه فحسب.
ما هو حقن SQL فعليًا
يحدث حقن SQL حين تُفسَّر البيانات التي يُدخلها المستخدم على أنها بنية SQL لا قيمة بيانات. تأمّل استعلام تسجيل دخول مبنيًا بتسلسل السلاسل:
إذا أدخل المستخدم ' OR '1'='1 كاسم مستخدم وأي كلمة مرور، يصبح الاستعلام الناتج:
الشرط '1'='1' صحيح دائمًا، فيُعيد الاستعلام كل صفوف المستخدمين — تجاوز كامل للمصادقة. أحمال أكثر خطورة قادرة على حذف جداول أو سرقة بيانات أو كتابة ملفات على القرص تبعًا لصلاحيات قاعدة البيانات.
PreparedStatement: التجميع المسبق والمعاملات
يُرسل PreparedStatement قالب SQL إلى خادم قاعدة البيانات أوّلًا، قبل أن تُعرف أي قيمة. يُحلّل الخادم الاستعلام ويُجمّع خطة تنفيذه عند تلك النقطة. ثم تُرسَل القيم لاحقًا كمعاملات مكتوبة بنوع محدد — ولا يمكنها قط تغيير بنية الاستعلام لأن البنية مُقفَلة أصلًا.
رمز المؤشر هو علامة الاستفهام ?. تُربط المعاملات بالموضع (يبدأ من 1) باستخدام دوال ضبط مكتوبة:
إن أدخل المهاجم ' OR '1'='1 كاسم مستخدم مجددًا، تتعامل قاعدة البيانات مع تلك السلسلة بالكامل كقيمة حرفية تُقارَن بعمود username — ولا تُحلَّل أبدًا كبنية SQL. لا يطابقها أي صف، فلا يحدث تجاوز.
دوال الضبط وسلامة النوع
يوفّر PreparedStatement دوال ضبط لكل نوع SQL. استخدام الدالة الصحيحة يُتيح للمشغّل تحويل النوع المناسب ويضمن أن مقارنات الأعمدة تستخدم دلالات النوع الصحيحة:
دوال الضبط الشائعة: setString وsetInt وsetLong وsetDouble وsetBigDecimal وsetBoolean وsetDate وsetTimestamp وsetNull. للأعمدة القابلة للقيمة الفارغة، استخدم دائمًا setNull(index, Types.VARCHAR) عوضًا عن تمرير null من Java إلى setString — فسلوك تمرير null يختلف بين المشغّلات.
إعادة استخدام PreparedStatement
من مزايا الأداء للاستعلامات المُجهَّزة أن خطة التجميع ذاتها تُعاد استخدامها لتنفيذات متعددة بمعاملات مختلفة. هذا مفيد بصفة خاصة لعمليات الإدراج الجماعي:
prepareStatement له تكلفة أولية (رحلة شبكية لتجميع الخطة). إن كنت تُدرج أو تُحدّث صفوفًا كثيرة في حلقة، جهِّز الاستعلام خارجها وارتبط بمعاملات جديدة داخلها. للدُفعات الكبيرة جدًا، ادمج هذا مع التنفيذ الدُفعي (درس لاحق).
استرجاع المفاتيح المولَّدة تلقائيًا
حين تُدرج صفًا في جدول ذي مفتاح أساسي يزداد تلقائيًا، غالبًا تحتاج المعرّف الناتج فورًا. مرّر العلَم Statement.RETURN_GENERATED_KEYS إلى prepareStatement، ثم استرجعه عبر getGeneratedKeys():
ما لا يحمي منه PreparedStatement
تحمي المعاملات القيمَ — البيانات التي تملأ المؤشرات. لكنها لا تحمي العناصر البنيوية الديناميكية كأسماء الجداول وأسماء الأعمدة واتجاهات ORDER BY، إذ لا يمكن إرسالها كمعاملات. إن احتجت جعل هذه عناصر ديناميكية، استخدم قائمة مسموح بها:
الخلاصة
يستغل حقن SQL إخفاق الفصل بين الكود والبيانات. يُطبّق PreparedStatement هذا الفصل على مستوى البروتوكول: يُجمَّع قالب SQL قبل إرسال أي بيانات مستخدم، مما يجعل استحالة قيام قيمة معامل بتغيير غرض الاستعلام أمرًا بنيويًا. استخدم دالة الضبط المكتوبة المناسبة لكل عمود، وجهِّز خارج الحلقات، واسترجع المفاتيح المولَّدة حين تحتاجها، وحصِّن الأجزاء البنيوية الديناميكية بقوائم مسموح بها.