التحقق من المدخلات بقائمة المسموح
البوابة الأولى: ماذا تقبل، وكيف تمنع ما لم تتوقّعه من التسلل؟ ⏱ نحو 40 دقيقة.
- تفرّق بين التحقق والتطهير.
- تتحقّق بقائمة سماح بالنوع والشكل والمدى.
- تمنع الإسناد الجماعي باختيار الحقول بالاسم.
- تضع التحقق في الخادم.
١التحقق أولًا، قبل كل شيء
التحقق من المدخلات هو البوابة الأولى: يرفض ما لا يطابق المتوقَّع قبل أن يصل إلى منطقك أو قاعدتك. يُكمّل الترميز والفصل (الدرس 4): هذان يحميان عند الإخراج، والتحقق يحمي عند الدخول.
| التحقق Validation | التطهير/الترميز | |
|---|---|---|
| يفعل | يقبل أو يرفض | يحوّل ليصبح آمنًا |
| المكان | عند الدخول | عند الإخراج لكل سياق |
| المثال | العمر عدد 0–120 | ترميز < في HTML |
القاعدة الأهم: تحقّق بـقائمة سماح (ما المسموح؟) لا بقائمة منع (ما الممنوع؟). قائمة المنع تنسى دائمًا حالة، أما قائمة السماح فترفض كل ما لم تُدرجه صراحة.
٢تحقق بالنوع والشكل والمدى
لكل حقل عقد: نوعه، وشكله، ومداه، وقائمة قيمه إن كانت محدودة. انظر تحققًا كاملًا، ثم جرّب كسره:
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const ROLES = ['customer', 'staff']; // قائمة سماح app.post('/users', (req, res) => { const { name, age, role } = req.body; // تحقق بالنوع والشكل والمدى وقائمة السماح if (typeof name !== 'string' || name.trim().length < 2 || name.length > 50) return res.status(400).json({ error: 'الاسم من 2 إلى 50 حرفًا' }); if (!Number.isInteger(age) || age < 0 || age > 120) return res.status(400).json({ error: 'العمر عدد صحيح بين 0 و 120' }); if (!ROLES.includes(role)) return res.status(400).json({ error: 'دور غير مسموح' }); // نبني الكائن بحقول مختارة بالاسم (ضد الإسناد الجماعي) res.status(201).json({ created: { name: name.trim(), age, role } });}); app.listen(3000);
الأول صحيح (201). والبقية رُفضت 400: اسم قصير، وعمر خارج المدى، ودور ليس في قائمة السماح. كل حقل يُفحص بعقده، وما يخالف يُرفض فورًا.
| البُعد | السؤال | مثال |
|---|---|---|
| النوع | نص؟ عدد؟ منطقي؟ | typeof age === "number" |
| الشكل | يطابق النمط؟ | بريد، هاتف، تاريخ |
| المدى | ضمن الحدود؟ | 0 <= age <= 120 |
| قائمة السماح | من القيم المسموحة؟ | ROLES.includes(role) |
٣الإسناد الجماعي ومكان التحقق
خطر خفيّ: نسخ جسم الطلب كله { ...req.body }. فيرسل المهاجم حقلًا لم تتوقّعه (role: "admin" أو status: "paid") فيدخل في كائنك. الدفاع: اختر الحقول المسموحة بالاسم، ولا تنسخ الجسم كله:
وفي العمل الحقيقي تُستخدم مكتبات تحقّق (schemas) تصف عقد كل حقل مرة وتفرضه، بدل كتابة الشروط يدويًّا لكل مسار.
توقّع وتحقّق
التحقق من المدخلات يحدث:
الأقوى:
نسخ ...req.body يسبب:
مكان التحقق الموثوق:
الخلاصة
- التحقق يرفض ما لا يطابق المتوقَّع عند الدخول، ويُكمّل الترميز عند الإخراج.
- تحقّق بقائمة سماح لا منع؛ وافحص النوع والشكل والمدى والقيم المسموحة.
- لا تنسخ
...req.body؛ اختر الحقول بالاسم ضد الإسناد الجماعي. - التحقق مكانه الخادم؛ تحقق الواجهة للتجربة فقط.
تحدٍّ برمجي: تحقّق من التذاكر
متقدممسار إنشاء التذكرة يقبل أي حقول وينسخ الجسم كله (إسناد جماعي) بلا تحقق. أصلحه: تحقّق من العنوان والأولوية، واختر الحقول بالاسم.
- العنوان نص من 3 إلى 100 حرف، وإلا 400.
- الأولوية من
['low', 'normal', 'high']، وإلا 400. - ابنِ التذكرة بحقول مختارة بالاسم، لا بنسخ الجسم.
👀 اعرض الاستجابات المطلوبة
POST /tickets{"title": "لا يعمل الدفع", "priority": "high"}POST /tickets{"title": "لا", "priority": "high"}POST /tickets{"title": "عطل", "priority": "urgent"}POST /tickets{"title": "محاولة", "priority": "low", "status": "closed", "isAdmin": true}
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- تذكرة صحيحة ← 201
- عنوان قصير ← 400
- أولوية غير مسموحة ← 400
- حالة التذكرة open رغم ما أُرسل
- لا نسخ لجسم الطلب كله
💡 تلميح 1
if (typeof title !== 'string' || title.trim().length < 3 …) return res.status(400)….💡 تلميح 2
if (!PRIORITIES.includes(priority)) ….💡 تلميح 3
{ title: title.trim(), priority, status: 'open' }.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const PRIORITIES = ['low', 'normal', 'high'];app.post('/tickets', (req, res) => { const { title, priority } = req.body; if (typeof title !== 'string' || title.trim().length < 3 || title.length > 100) return res.status(400).json({ error: 'العنوان من 3 إلى 100 حرف' }); if (!PRIORITIES.includes(priority)) return res.status(400).json({ error: 'أولوية غير مسموحة' }); const ticket = { title: title.trim(), priority, status: 'open' }; // حقول مختارة بالاسم res.status(201).json({ ticket });});app.listen(3000);