الاستعلامات المعلَّمة
علامة ? تحمي القيم، فماذا يحمي اسم العمود الذي يختاره المستخدم؟ ⏱ نحو 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:
الخلاصة
- علامات ? تُعلّم القيم فقط، في كل العمليات لا تسجيل الدخول وحده.
- اسم العمود والترتيب لا يُعلَّمان؛ اخترهما من قائمة مسموحة ثابتة.
ORMيعلّم القيم تلقائيًّا، لكن الاستعلام الخام يعيد الخطر.- القاعدة: القيمة تُعلَّم، والبنية تُختار من مجموعة تحددها أنت.
تحدٍّ برمجي: استعلامات معلَّمة كاملة
متقدمهذا الخادم يبني كل استعلاماته بالدمج النصّي: المعرّف، والبحث، والترتيب. أمّنها: القيم بالتعليم، والترتيب بقائمة مسموحة.
- قراءة المنتج بالمعرّف: استعلام معلَّم.
- البحث: LIKE معلَّم.
- الترتيب: عمود من قائمة مسموحة (id أو name أو price)، وغيرها يسقط إلى id دون خطأ.
👀 اعرض الاستجابات المطلوبة
GET /products/2GET /search?q=سماعةGET /search?sort=price&q=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);