الدرس 19 · الوحدة 9: تقييم وتحسين تصميم صلاحيات المستخدمين
مراجعة تصميم الصلاحيات: الثغرات الشائعة
كيف تقيّم تصميم صلاحيات قائم، وتكتشف ثغراته في التصميم والكود، وتكتب تقرير مراجعة؟ ⏱ نحو 45 دقيقة.
بعد هذا الدرس ستستطيع أن:
- تتعرف على ثغرات تصميم الصلاحيات: التراكم، والانفجار، والدور الخارق، وغياب فصل المهام، والحسابات اليتيمة.
- تتعرف على ثغرات التنفيذ: الثقة بالعميل، والإسناد الجماعي، والمسار المنسي.
- تراجع نظامًا بقائمة تدقيق، وتكتب تقريرًا مرتبًا حسب الخطورة.
- تصلح ثغرات حقيقية في خادم.
١لماذا نراجع تصميم الصلاحيات؟
التصميم الأول نادرًا ما يكون كاملًا، والأنظمة تتغير: أدوار تُضاف، وموظفون ينتقلون، ومسارات تُبرمج على عجل. من الخطة: «تقييم تصميم الصلاحيات الحالي وتحليله لضمان التناسب والتعامل مع التهديدات». المراجعة تفحص التصميم (المصفوفة والأدوار) والتنفيذ (الكود) معًا.
٢ثغرات في التصميم
| الثغرة | كيف تظهر | المعالجة |
|---|---|---|
| تراكم الصلاحيات | موظفة انتقلت من التسجيل إلى المحاسبة وبقيت لها صلاحيات الدورين | مراجعة دورية للإسناد، وسحب الدور القديم عند النقل |
| انفجار الأدوار | أربعون دورًا لخمسين موظفًا، لا أحد يعرف الفرق بينها | دمج الأدوار المتشابهة، والاستثناءات بشروط لا بأدوار |
| دور خارق | دور «مدير» يملك كل شيء ويستخدمه كثيرون للسهولة | تقسيمه، وحصره بأشخاص قليلين، وتسجيل كل استخدام |
| غياب فصل المهام | الشخص نفسه يطلب الاسترداد ويعتمده | صلاحيتان منفصلتان لدورين مختلفين |
| حسابات يتيمة | حسابات موظفين غادروا ما زالت فعالة | إجراء إغلاق الحساب مع إنهاء العلاقة |
٣ثغرات في التنفيذ
| الثغرة | مثال | الصواب |
|---|---|---|
| الثقة بما يرسله العميل | الدور في ترويسة أو حقل يرسله المتصفح: x-role: manager | الدور من قاعدة البيانات عبر الرمز فقط |
| الإسناد الجماعي | Object.assign(user, req.body) فيغيّر المستخدم دوره بنفسه | تحديث الحقول المسموحة بالاسم |
| مسار منسي | مسار الحذف بلا authorize لأنه أُضيف أخيرًا | المنع افتراضيًّا، واختبار كل مسار |
| الحماية في الواجهة فقط | الزر مخفي والمسار مفتوح | التحقق في الخادم دائمًا |
| رسائل خطأ كاشفة | «كلمة المرور خاطئة لهذا البريد» تكشف أن البريد مسجل | رسالة عامة: «بيانات الدخول غير صحيحة» |
٤قائمة تدقيق وتقرير مراجعة
المراجعة المنهجية تعتمد قائمة ثابتة تمر عليها في كل مرة، ثم تقرير يرتب ما وجدته حسب خطورته:
☑️ قائمة تدقيق الصلاحيات
- هل لكل مسار في الخادم قاعدة صلاحية صريحة؟ (قارن قائمة المسارات بالمصفوفة)
- هل تأتي الهوية والدور من الرمز وحده؟
- هل تُفحص الملكية في كل مسار فيه معرّف
/:id؟ - هل تُحدَّث الحقول المسموحة فقط؟
- هل يملك كل دور الحد الأدنى؟ وهل يوجد دور خارق؟
- هل العمليات الحساسة مقسمة بين دورين؟
- هل تُسجَّل العمليات الحساسة؟ ومتى رُوجع الإسناد آخر مرة؟
📋 تقرير مراجعة: نظام «دورات» · نوفمبر 2026
| # | الملاحظة | الخطورة | التوصية |
|---|---|---|---|
| 1 | الدور يُقرأ من ترويسة x-role | حرجة | إزالة الترويسة وقراءة الدور من المستخدم |
| 2 | PATCH /api/me يقبل أي حقل | حرجة | تحديث الاسم فقط |
| 3 | DELETE /api/courses/:id بلا تفويض | عالية | إضافة course:delete للمدير فقط |
| 4 | ثلاثة موظفين بدور مدير | متوسطة | حصره في أ. فهد ونائبه |
توقّع وتحقّق
موظفة انتقلت إلى المحاسبة وبقيت لها صلاحيات التسجيل. هذه:
الخادم يقرأ الدور من ترويسة يرسلها المتصفح. الخطورة:
أفضل رسالة عند فشل تسجيل الدخول:
لا تكشف وجود البريد.
كيف تتأكد أنه لا يوجد مسار منسي؟
الخلاصة
- المراجعة تفحص التصميم والتنفيذ معًا، وتتكرر دوريًّا.
- التصميم: تراكم الصلاحيات، وانفجار الأدوار، والدور الخارق، وفصل المهام، والحسابات اليتيمة.
- التنفيذ: الدور من الرمز لا من العميل، والحقول المسموحة فقط، ولا مسار بلا قاعدة.
- تقرير المراجعة: ملاحظة، وخطورة، وتوصية؛ والحرجة أولًا.
تحدٍّ برمجي: أصلح ما وجدته المراجعة
متقدمهذا هو الخادم الذي كُتب عنه تقرير المراجعة في الدرس. أصلح الملاحظات الثلاث الأولى.
المطلوب:
- الدور يأتي من بيانات المستخدم فقط؛ لا أثر لترويسة
x-role. PATCH /api/meيحدّث الاسم فقط، ولا يغيّر الدور أبدًا.- حذف الدورة يتطلب
course:delete(للمدير فقط).
👀 اعرض الاستجابات المطلوبة
✓ التحقق الآلي: يشغّل المختبر خادمك من جديد ويرسل إليه هذه الطلبات بالترتيب ثم يفحص الردود
GET /api/reports/finance🔑PATCH /api/me{"name": "سارة أحمد", "role": "manager"} 🔑GET /api/reports/finance🔑DELETE /api/courses/1🔑DELETE /api/courses/1🔑
- الخادم يعمل ويرد على كل الطلبات بلا أخطاء غير معالَجة
- الترويسة
x-roleلا تمنح سارة التقرير ← 403 - الاسم تغيّر والدور بقي trainee
- بعد محاولة الترقية ما زالت سارة ممنوعة ← 403
- سارة لا تحذف دورة ← 403
- أ. فهد يحذف ← deleted
- لا قراءة لترويسة
x-role - لا Object.assign مع req.body
💡 تلميح 1
في authenticate أعد السطر الأصلي:
req.user = user;💡 تلميح 2
في /
api/me: if (req.body.name) u.name = req.body.name;💡 تلميح 3
أضف
authorize('course:delete') إلى مسار الحذف؛ والصلاحية موجودة للمدير في ROLE_PERMS.حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
const express = require('express');const app = express();app.use(express.json()); // من مصفوفة الصلاحيات (الدرس 16) إلى الكود مباشرةconst ROLE_PERMS = { trainee: ['course:read', 'enrollment:create', 'enrollment:read:own'], trainer: ['course:read', 'attendance:record'], registrar: ['course:read', 'enrollment:read', 'enrollment:approve'], manager: ['course:read', 'course:delete', 'enrollment:read', 'report:finance', 'user:manage'],};const users = [ { id: 1, name: 'سارة', role: 'trainee' }, { id: 2, name: 'نورة', role: 'registrar' }, { id: 3, name: 'أ. فهد', role: 'manager' }, { id: 4, name: 'خالد', role: 'trainer' },];// رموز دخول مبسطة للتجربة؛ في النظام الحقيقي JWT كما في مقرر أطر الويبconst tokens = { 'tok-sara': 1, 'tok-noura': 2, 'tok-fahad': 3, 'tok-khaled': 4 }; // 1) المصادقة: من أنت؟function authenticate(req, res, next) { const t = (req.headers.authorization || '').replace('Bearer ', ''); const user = users.find(u => u.id === tokens[t]); if (!user) return res.status(401).json({ error: 'سجّل الدخول أولًا' }); req.user = user; next();} // 2) التفويض: هل يحق لك؟const can = (user, perm) => (ROLE_PERMS[user.role] || []).includes(perm);function authorize(perm) { return (req, res, next) => can(req.user, perm) ? next() : res.status(403).json({ error: 'لا تملك الصلاحية ' + perm });} const courses = [{ id: 1, title: 'تصميم الويب' }, { id: 2, title: 'قواعد البيانات' }]; app.get('/api/reports/finance', authenticate, authorize('report:finance'), (req, res) => { res.json({ revenue: 12500 });}); app.patch('/api/me', authenticate, (req, res) => { const u = users.find(x => x.id === req.user.id); if (req.body.name) u.name = req.body.name; // الحقول المسموحة فقط res.json(u);}); app.delete('/api/courses/:id', authenticate, authorize('course:delete'), (req, res) => { const i = courses.findIndex(c => c.id === Number(req.params.id)); if (i < 0) return res.status(404).json({ error: 'غير موجودة' }); courses.splice(i, 1); res.json({ deleted: true });}); app.listen(3000);