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

كلمات المرور والتفويض و IDOR

كيف نخزّن كلمات المرور بأمان، ولماذا لا يكفي أن نعرف من المستخدم؟ ⏱ نحو 50 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تجزّئ كلمات المرور بـ bcrypt بدل تخزينها.
  • تفرّق بين المصادقة والتفويض.
  • تكتشف ثغرة IDOR وتصلحها بشرط الملكية.
  • تختار رمز الحالة المناسب (401/403/404).

١لا تخزّن كلمة المرور قط

فئة A02 (الإخفاقات التشفيرية): تخزين كلمات المرور نصًّا صريحًا كارثة؛ تسريب القاعدة يكشفها كلها. الحل ألا نخزّنها أصلًا، بل بصمتها بدالة تجزئة بطيئة ومملّحة مثل bcrypt:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const bcrypt = require('bcryptjs'); app.post('/register', (req, res) => {  const hash = bcrypt.hashSync(req.body.pass, 10);   // لا نخزّن كلمة المرور، بل بصمتها  res.json({ stored: hash.slice(0, 29) + '…' });      // لاحظ: لا تشبه كلمة المرور}); app.post('/login', (req, res) => {  const stored = bcrypt.hashSync('s@rah123', 10);     // مثال: ما كان مخزَّنًا  const ok = bcrypt.compareSync(req.body.pass, stored);  res.json({ granted: ok });}); app.listen(3000);

عند التسجيل نخزّن البصمة لا الكلمة. وعند الدخول نقارن بـ compareSync. لا يمكن عكس البصمة إلى الكلمة، والمقارنة نفسها ثابتة الزمن تمنع تسريب الوقت.

سيئجيد
التخزيننص صريحbcrypt.hash (أو argon2/scrypt)
السرعةدالة سريعة (MD5/SHA)بطيئة عمدًا لتعسير التخمين
الملحبلا ملحملح لكل مستخدم (bcrypt يضيفه)
المقارنة==مقارنة ثابتة الزمن

لماذا بطيئة؟ لأن المهاجم إن سرّب البصمات حاول تخمينها بتجربة ملايين الكلمات؛ فكل تجزئة بطيئة تضاعف كلفة التخمين عليه.

٢مصادقة أم تفويض؟

مفهومان يُخلط بينهما كثيرًا، وكلاهما ضروري:

المصادقة Authenticationالتفويض Authorization
السؤالمن أنت؟ماذا يحق لك؟
المثالتسجيل الدخولهل تملك صلاحية الحذف؟
يأتيأولًابعد المصادقة
الرمز عند الفشل401403

الخطأ الشائع التحقق من المصادقة فقط ونسيان التفويض: «أنت مسجّل الدخول، تفضّل أي شيء». وهنا تنشأ ثغرة التحكم بالوصول، وأشهرها IDOR.

٣IDOR: التحقق من الدخول لا يكفي

IDOR (الوصول المباشر غير الآمن): الخادم يتحقق أنك مسجّل، ثم يعطيك أي مورد بمعرّفه دون أن يسأل: هل هو لك؟ سارة تملك الطلب 1، فماذا لو طلبت 2؟

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const orders = [  { id: 1, userId: 1, total: 330 },  { id: 2, userId: 2, total: 90 },];const SESSIONS = { 'tok-sara': 1, 'tok-khaled': 2 };function auth(req, res, next) {  const id = SESSIONS[(req.headers.authorization || '').replace('Bearer ', '')];  if (!id) return res.status(401).json({ error: 'سجّل الدخول' });  req.userId = id; next();} app.get('/orders/:id', auth, (req, res) => {  const o = orders.find(x => x.id === Number(req.params.id));  if (!o) return res.status(404).json({ error: 'غير موجود' });  res.json(o);                       // ✗ لا يتحقق أن الطلب يخص المستخدم});app.listen(3000);

الطلب الثاني أعاد طلب خالد لسارة! التحقق كان من الدخول فقط. يكفي أن يجرّب المهاجم الأرقام ليجمع طلبات الجميع.

الإصلاح: اجعل الملكية شرطًا، وردّ 404 لا 403 كيلا تؤكد وجود المورد:

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const orders = [  { id: 1, userId: 1, total: 330 },  { id: 2, userId: 2, total: 90 },];const SESSIONS = { 'tok-sara': 1, 'tok-khaled': 2 };function auth(req, res, next) {  const id = SESSIONS[(req.headers.authorization || '').replace('Bearer ', '')];  if (!id) return res.status(401).json({ error: 'سجّل الدخول' });  req.userId = id; next();} app.get('/orders/:id', auth, (req, res) => {  const o = orders.find(x => x.id === Number(req.params.id));  if (!o || o.userId !== req.userId)    return res.status(404).json({ error: 'غير موجود' });   // الملكية شرط، و404 لإخفاء الوجود  res.json(o);});app.listen(3000);

الآن سارة ترى طلبها فقط، وطلب خالد يرد 404. القاعدة: لكل وصول لمورد، تحقق أنه يخص صاحب الطلب أو أنه يملك صلاحية عامة عليه.

توقّع وتحقّق

كلمات المرور تُخزَّن:

«ماذا يحق لك؟» سؤال:

IDOR سببه:

لماذا 404 لا 403 لمورد لا تملكه؟

الخلاصة

تحدٍّ برمجي: أصلح IDOR في الطلبات

متقدم

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

المطلوب:
  • عرض طلب بمعرّفه: لصاحبه فقط، وغيره 404.
  • قائمة الطلبات: طلبات المستخدم الحالي فقط.
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
  1. GET /orders/1 🔑
  2. GET /orders/2 🔑
  3. GET /orders/2 🔑
  4. GET /orders 🔑
  • الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
  • سارة ترى طلبها 1
  • سارة لا ترى طلب خالد ← 404
  • خالد يرى طلبه 2
  • قائمة سارة فيها طلب واحد
  • يتحقق من الملكية بـ userId

💡 تلميح 1
لعرض الطلب: if (!o || o.userId !== req.userId) return res.status(404)….
💡 تلميح 2
للقائمة: orders.filter(o => o.userId === req.userId).
💡 تلميح 3
الملكية شرط في كل وصول لمورد يخص مستخدمًا.

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

const express = require('express');const app = express();app.use(express.json());app.use(express.urlencoded({ extended: false })); const orders = [  { id: 1, userId: 1, total: 330 },  { id: 2, userId: 2, total: 90 },];const SESSIONS = { 'tok-sara': 1, 'tok-khaled': 2 };function auth(req, res, next) {  const id = SESSIONS[(req.headers.authorization || '').replace('Bearer ', '')];  if (!id) return res.status(401).json({ error: 'سجّل الدخول' });  req.userId = id; next();} app.get('/orders/:id', auth, (req, res) => {  const o = orders.find(x => x.id === Number(req.params.id));  if (!o || o.userId !== req.userId)    return res.status(404).json({ error: 'غير موجود' });  res.json(o);});app.get('/orders', auth, (req, res) => {  res.json(orders.filter(o => o.userId === req.userId));});app.listen(3000);
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

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