كلمات المرور والتفويض و 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 | |
|---|---|---|
| السؤال | من أنت؟ | ماذا يحق لك؟ |
| المثال | تسجيل الدخول | هل تملك صلاحية الحذف؟ |
| يأتي | أولًا | بعد المصادقة |
| الرمز عند الفشل | 401 | 403 |
الخطأ الشائع التحقق من المصادقة فقط ونسيان التفويض: «أنت مسجّل الدخول، تفضّل أي شيء». وهنا تنشأ ثغرة التحكم بالوصول، وأشهرها 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 لمورد لا تملكه؟
الخلاصة
- لا تخزّن كلمة المرور؛ خزّن بصمتها بدالة بطيئة مملّحة مثل
bcrypt. - المصادقة «من أنت»
(401)، والتفويض «ماذا يحق لك»(403)؛ كلاهما لازم. IDOR: مورد يُعرَض بمعرّفه دون التحقق من ملكيته.- الإصلاح: الملكية شرط لكل وصول، والرد 404 لإخفاء وجود مورد الغير.
تحدٍّ برمجي: أصلح IDOR في الطلبات
متقدمهذا الخادم يصادق المستخدمين، لكنه يعرض أي طلب بمعرّفه، ويرجع طلبات الجميع في القائمة. أصلح الثغرتين بشرط الملكية.
- عرض طلب بمعرّفه: لصاحبه فقط، وغيره 404.
- قائمة الطلبات: طلبات المستخدم الحالي فقط.
👀 اعرض الاستجابات المطلوبة
GET /orders/1🔑GET /orders/2🔑GET /orders/2🔑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);