طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 243 · أمن الويب
الدرس 9 · الوحدة 5: تصميم تطبيقات مقاومة للهجمات

الاستعلامات المعلَّمة

علامة ? تحمي القيم، فماذا يحمي اسم العمود الذي يختاره المستخدم؟ ⏱ نحو 45 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تعلّم القيم في كل عمليات CRUD.
  • تؤمّن ما لا يُعلَّم (الترتيب) بقائمة مسموحة.
  • تستخدم ORM بوصفه طبقة تعليم تلقائي.
  • تتجنّب خطر الاستعلام الخام.

١الاستعلام المعلَّم في كل العمليات

في الدرس السابق أصلحنا تسجيل الدخول. المبدأ نفسه يسري على كل استعلام يمسّ مدخل المستخدم: قراءة، وإدراج، وتعديل، وحذف. القاعدة ثابتة: القيم بعلامات ? ومصفوفة منفصلة.

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const sqlite3 = require('sqlite3');const db = new sqlite3.Database(':memory:');db.serialize(() => {  db.run(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, pass TEXT, role TEXT, email TEXT)`);  db.run(`INSERT INTO users (name, pass, role, email) VALUES    ('سارة', 's@rah123', 'customer', 'sara@mail.sa'),    ('خالد', 'kh@led456', 'customer', 'khaled@mail.sa'),    ('المدير', 'Adm!n#2025', 'admin', 'admin@mail.sa')`);  db.run(`CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)`);  db.run(`INSERT INTO products (name, price) VALUES    ('سماعة', 120), ('لوحة مفاتيح', 90), ('كاميرا ويب', 210)`);}); // كل عملية تمرّر القيم منفصلة بعلامات ?app.get('/products/:id', (req, res) =>  db.get("SELECT * FROM products WHERE id = ?", [req.params.id], (e, row) =>    row ? res.json(row) : res.status(404).json({ error: 'غير موجود' }))); app.post('/products', (req, res) =>  db.run("INSERT INTO products (name, price) VALUES (?, ?)", [req.body.name, req.body.price], function (e) {    e ? res.status(400).json({ error: 'بيانات خاطئة' }) : res.status(201).json({ id: this.lastID }); })); app.listen(3000);

القراءة بالمعرّف والإدراج كلاهما معلَّم. حتى المعرّف في الرابط مدخل من المستخدم، فلا يُدمج نصًّا.

٢ما لا يمكن تعليمه: الترتيب الديناميكي

أحيانًا يختار المستخدم عمود الترتيب. هذا اسم عمود لا قيمة، فلا يُعلَّم. والحل: لا تثق بالاسم مباشرة؛ قابله بـقائمة مسموحة ثابتة، فإن لم يكن منها استخدم قيمة افتراضية آمنة:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const sqlite3 = require('sqlite3');const db = new sqlite3.Database(':memory:');db.serialize(() => {  db.run(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, pass TEXT, role TEXT, email TEXT)`);  db.run(`INSERT INTO users (name, pass, role, email) VALUES    ('سارة', 's@rah123', 'customer', 'sara@mail.sa'),    ('خالد', 'kh@led456', 'customer', 'khaled@mail.sa'),    ('المدير', 'Adm!n#2025', 'admin', 'admin@mail.sa')`);  db.run(`CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)`);  db.run(`INSERT INTO products (name, price) VALUES    ('سماعة', 120), ('لوحة مفاتيح', 90), ('كاميرا ويب', 210)`);}); app.get('/products', (req, res) => {  // ✓ اسم العمود لا يُعلَّم؛ يُختار من قائمة مسموحة  const allowed = { price: 'price', name: 'name' };  const col = allowed[req.query.sort] || 'id';  const dir = req.query.dir === 'desc' ? 'DESC' : 'ASC';  db.all(`SELECT id, name, price FROM products ORDER BY ${col} ${dir}`, [], (e, rows) => res.json(rows));}); app.listen(3000);

الترتيب بالسعر عمل. والطلب الثاني بعمود خبيث سقط إلى الترتيب الافتراضي id؛ لأن 'DROP TABLE' ليس في القائمة المسموحة فلم يصل إلى الاستعلام أصلًا.

هذه قاعدة عامة: ما لا يُعلَّم (اسم جدول، عمود، اتجاه ترتيب)، اختره من مجموعة ثابتة تحددها أنت، لا من نص المستخدم.

٣طبقتا ORM والإجراءات المخزّنة

أطر مثل Sequelize (من مقرر أطر الويب) تبني الاستعلامات معلَّمة تلقائيًّا، فتقلّل الخطأ البشري:

الطريقةآمنة افتراضيًّا؟ملحوظة
db.get(sql, [v])نعم مع ?الأساس الذي نتعلّمه
Product.findByPk(id)نعمORM يعلّم القيم تلقائيًّا
Product.findAll({ where: { name } })نعمآمن
sequelize.query("… " + input)لاالاستعلام الخام يعيد الخطر
إجراء مخزّن بمعاملاتنعمفي PostgreSQL/Oracle (مقرر 122)

توقّع وتحقّق

علامات ? تُعلّم:

عمود الترتيب من المستخدم يُؤمَّن بـ:

ORM مثل Sequelize:

الاستعلام الخام في ORM:

الخلاصة

تحدٍّ برمجي: استعلامات معلَّمة كاملة

متقدم

هذا الخادم يبني كل استعلاماته بالدمج النصّي: المعرّف، والبحث، والترتيب. أمّنها: القيم بالتعليم، والترتيب بقائمة مسموحة.

المطلوب:
  • قراءة المنتج بالمعرّف: استعلام معلَّم.
  • البحث: LIKE معلَّم.
  • الترتيب: عمود من قائمة مسموحة (id أو name أو price)، وغيرها يسقط إلى id دون خطأ.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /products/2
  2. GET /search?q=سماعة
  3. GET /search?sort=price&q=
  4. GET /search?sort=name;DROP TABLE products&q=
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • قراءة المنتج 2 بالمعرّف
  • البحث يرجع نتيجة واحدة
  • الترتيب بعمود مسموح يعمل ← 200
  • ترتيب بعمود خبيث لا يكسر الخادم ← 200
  • وما زال يرجع مصفوفة لا خطأ
  • لا دمج نصّي للمعرّف

💡 تلميح 1
القراءة: "… WHERE id = ?" مع [req.params.id].
💡 تلميح 2
الترتيب: const allowed = { id:'id', name:'name', price:'price' }; const col = allowed[req.query.sort] || 'id';.
💡 تلميح 3
البحث: LIKE ? مع ['%'+q+'%']، والترتيب بـ col المختار من القائمة.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const sqlite3 = require('sqlite3');const db = new sqlite3.Database(':memory:');db.serialize(() => {  db.run(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, pass TEXT, role TEXT, email TEXT)`);  db.run(`INSERT INTO users (name, pass, role, email) VALUES    ('سارة', 's@rah123', 'customer', 'sara@mail.sa'),    ('خالد', 'kh@led456', 'customer', 'khaled@mail.sa'),    ('المدير', 'Adm!n#2025', 'admin', 'admin@mail.sa')`);  db.run(`CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)`);  db.run(`INSERT INTO products (name, price) VALUES    ('سماعة', 120), ('لوحة مفاتيح', 90), ('كاميرا ويب', 210)`);}); app.get('/products/:id', (req, res) => {  db.get("SELECT * FROM products WHERE id = ?", [req.params.id], (e, row) =>    row ? res.json(row) : res.status(404).json({ error: 'غير موجود' }));}); app.get('/search', (req, res) => {  const allowed = { id: 'id', name: 'name', price: 'price' };  const col = allowed[req.query.sort] || 'id';  db.all("SELECT * FROM products WHERE name LIKE ? ORDER BY " + col,    ['%' + (req.query.q || '') + '%'],    (e, rows) => e ? res.status(400).json({ error: String(e.message) }) : res.json(rows));}); app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس