JDBC وقواعد البيانات

PreparedStatement والحماية من حقن SQL

15 دقيقة الدرس 4 من 13

PreparedStatement والحماية من حقن SQL

في الدرس السابق أرسلتَ نصوص SQL مبنيّة بتسلسل السلاسل النصية. هذا النهج يعمل — حتى يقدّم أحدهم مدخلات خبيثة. يتناول هذا الدرس PreparedStatement، الآلية المعيارية في JDBC للاستعلامات ذات المعاملات، ويشرح بدقة لماذا المعاملات تُقضي على حقن SQL تمامًا لا تُخفّفه فحسب.

ما هو حقن SQL فعليًا

يحدث حقن SQL حين تُفسَّر البيانات التي يُدخلها المستخدم على أنها بنية SQL لا قيمة بيانات. تأمّل استعلام تسجيل دخول مبنيًا بتسلسل السلاسل:

// خطير — لا تفعل هذا أبدًا String username = request.getParam("username"); String password = request.getParam("password"); String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql);

إذا أدخل المستخدم ' OR '1'='1 كاسم مستخدم وأي كلمة مرور، يصبح الاستعلام الناتج:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'anything'

الشرط '1'='1' صحيح دائمًا، فيُعيد الاستعلام كل صفوف المستخدمين — تجاوز كامل للمصادقة. أحمال أكثر خطورة قادرة على حذف جداول أو سرقة بيانات أو كتابة ملفات على القرص تبعًا لصلاحيات قاعدة البيانات.

لا تبنِ سلاسل SQL من مدخلات المستخدم أبدًا. حتى المدخلات البسيطة كمعرّف رقمي قابلة للحقن إن لم تتحكم في مصدر البيانات. قاعدة صارمة — استخدم دائمًا الاستعلامات ذات المعاملات — أأمن من محاولة التحقق من كل مدخل أو الهروب منه.

PreparedStatement: التجميع المسبق والمعاملات

يُرسل PreparedStatement قالب SQL إلى خادم قاعدة البيانات أوّلًا، قبل أن تُعرف أي قيمة. يُحلّل الخادم الاستعلام ويُجمّع خطة تنفيذه عند تلك النقطة. ثم تُرسَل القيم لاحقًا كمعاملات مكتوبة بنوع محدد — ولا يمكنها قط تغيير بنية الاستعلام لأن البنية مُقفَلة أصلًا.

رمز المؤشر هو علامة الاستفهام ?. تُربط المعاملات بالموضع (يبدأ من 1) باستخدام دوال ضبط مكتوبة:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); // المعامل رقم 1 ps.setString(2, password); // المعامل رقم 2 try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { System.out.println("تمت المصادقة: " + rs.getString("username")); } else { System.out.println("بيانات اعتماد غير صحيحة"); } } }

إن أدخل المهاجم ' OR '1'='1 كاسم مستخدم مجددًا، تتعامل قاعدة البيانات مع تلك السلسلة بالكامل كقيمة حرفية تُقارَن بعمود username — ولا تُحلَّل أبدًا كبنية SQL. لا يطابقها أي صف، فلا يحدث تجاوز.

لماذا يعمل هذا على مستوى البروتوكول: تُطبّق معظم برامج تشغيل JDBC البروتوكول الثنائي (wire) للاستعلامات المُجهَّزة. يسافر قالب SQL وقيم المعاملات في حزم شبكة منفصلة. يرى مُحلِّل SQL في الخادم القالبَ فقط — وقد أنهى تحليله قبل أن تصل القيم.

دوال الضبط وسلامة النوع

يوفّر PreparedStatement دوال ضبط لكل نوع SQL. استخدام الدالة الصحيحة يُتيح للمشغّل تحويل النوع المناسب ويضمن أن مقارنات الأعمدة تستخدم دلالات النوع الصحيحة:

PreparedStatement ps = conn.prepareStatement( "INSERT INTO orders (customer_id, amount, placed_at, active) VALUES (?, ?, ?, ?)" ); ps.setInt(1, 42); // عمود INT ps.setBigDecimal(2, new BigDecimal("199.95")); // عمود DECIMAL ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now())); // عمود TIMESTAMP ps.setBoolean(4, true); // عمود BOOLEAN / TINYINT(1) int rowsInserted = ps.executeUpdate(); System.out.println("صفوف مُدرجة: " + rowsInserted); ps.close();

دوال الضبط الشائعة: setString وsetInt وsetLong وsetDouble وsetBigDecimal وsetBoolean وsetDate وsetTimestamp وsetNull. للأعمدة القابلة للقيمة الفارغة، استخدم دائمًا setNull(index, Types.VARCHAR) عوضًا عن تمرير null من Java إلى setString — فسلوك تمرير null يختلف بين المشغّلات.

إعادة استخدام PreparedStatement

من مزايا الأداء للاستعلامات المُجهَّزة أن خطة التجميع ذاتها تُعاد استخدامها لتنفيذات متعددة بمعاملات مختلفة. هذا مفيد بصفة خاصة لعمليات الإدراج الجماعي:

String insertSql = "INSERT INTO products (name, price, stock) VALUES (?, ?, ?)"; List<Product> products = loadProductsFromFile(); // افتراضي try (PreparedStatement ps = conn.prepareStatement(insertSql)) { for (Product p : products) { ps.setString(1, p.name()); ps.setBigDecimal(2, p.price()); ps.setInt(3, p.stock()); ps.executeUpdate(); // يُعيد استخدام الخطة المُجمَّعة في كل تكرار } }
جهِّز مرّة، نفِّذ مرات عديدة. استدعاء prepareStatement له تكلفة أولية (رحلة شبكية لتجميع الخطة). إن كنت تُدرج أو تُحدّث صفوفًا كثيرة في حلقة، جهِّز الاستعلام خارجها وارتبط بمعاملات جديدة داخلها. للدُفعات الكبيرة جدًا، ادمج هذا مع التنفيذ الدُفعي (درس لاحق).

استرجاع المفاتيح المولَّدة تلقائيًا

حين تُدرج صفًا في جدول ذي مفتاح أساسي يزداد تلقائيًا، غالبًا تحتاج المعرّف الناتج فورًا. مرّر العلَم Statement.RETURN_GENERATED_KEYS إلى prepareStatement، ثم استرجعه عبر getGeneratedKeys():

String sql = "INSERT INTO articles (title, body) VALUES (?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, "JDBC Deep Dive"); ps.setString(2, "PreparedStatement prevents SQL injection by..."); ps.executeUpdate(); try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { long newId = keys.getLong(1); System.out.println("معرّف المقال الجديد: " + newId); } } }

ما لا يحمي منه PreparedStatement

تحمي المعاملات القيمَ — البيانات التي تملأ المؤشرات. لكنها لا تحمي العناصر البنيوية الديناميكية كأسماء الجداول وأسماء الأعمدة واتجاهات ORDER BY، إذ لا يمكن إرسالها كمعاملات. إن احتجت جعل هذه عناصر ديناميكية، استخدم قائمة مسموح بها:

// قائمة مسموح بها للـ ORDER BY الديناميكي — لا تُدرج مدخلات المستخدم الخام أبدًا هنا private static final Set<String> ALLOWED_COLUMNS = Set.of("name", "price", "created_at"); public List<Product> findAll(String orderBy) { if (!ALLOWED_COLUMNS.contains(orderBy)) { throw new IllegalArgumentException("عمود ترتيب غير صالح: " + orderBy); } // آمن للإدراج — نحن نتحكم في القيمة String sql = "SELECT * FROM products ORDER BY " + orderBy; // ... }

الخلاصة

يستغل حقن SQL إخفاق الفصل بين الكود والبيانات. يُطبّق PreparedStatement هذا الفصل على مستوى البروتوكول: يُجمَّع قالب SQL قبل إرسال أي بيانات مستخدم، مما يجعل استحالة قيام قيمة معامل بتغيير غرض الاستعلام أمرًا بنيويًا. استخدم دالة الضبط المكتوبة المناسبة لكل عمود، وجهِّز خارج الحلقات، واسترجع المفاتيح المولَّدة حين تحتاجها، وحصِّن الأجزاء البنيوية الديناميكية بقوائم مسموح بها.