مقدمة
المصادقة تجيب: من أنت؟ والتفويض يجيب: ماذا يحق لك أن تفعل بهذا المورد؟ معظم الثغرات الخطيرة في تطبيقات الويب اليوم تفويضية: عضو يحذف مشاركة غيره بتغيير رقم في الرابط، أو مشرف قسم يعدّل قسماً لا يشرف عليه. RBAC المصمم جيداً يجعل هذه الأخطاء نادرة.
أهداف التعلم
- الفصل بين الأدوار والصلاحيات.
- فحص الصلاحية على الخادم في كل عملية.
- تقييد الصلاحيات بنطاق (قسم، مشروع).
- منع رفع الصلاحيات (Privilege escalation) وثغرات IDOR.
1. صلاحيات صغيرة، أدوار تجمعها
const PERMISSIONS = ['posts.moderate', 'topics.move', 'users.ban', 'roles.manage'] as const;
type Permission = (typeof PERMISSIONS)[number];
const ROLES: Record<string, { rank: number; permissions: Permission[] }> = {
forum_moderator: { rank: 50, permissions: ['posts.moderate', 'topics.move'] },
admin: { rank: 80, permissions: ['posts.moderate', 'topics.move', 'users.ban'] },
};
افحص في الكود الصلاحية لا اسم الدور: can(user, 'posts.moderate') بدل user.role === 'admin'. هكذا تضيف أدواراً جديدة دون تعديل كل مكان في الكود.
2. الخادم يفحص كل مرة
إخفاء زر «حذف» في الواجهة تجربة مستخدم، وليس حماية. أي شخص يستطيع إرسال الطلب يدوياً:
export async function deletePost(viewer: Viewer, postId: string) {
const post = await db.post.findUnique({ where: { id: postId }, include: { topic: true } });
if (!post) throw notFound();
const own = post.authorId === viewer.userId;
const canModerate = await can(viewer, 'posts.moderate', { nodeId: post.topic.nodeId });
if (!own && !canModerate) throw notFound(); // 404 لا يكشف وجود المورد
// ...
}
لاحظ أن الفحص مرتبط بالمورد المحدد: ليس «هل يحق له حذف المشاركات؟» بل «هل يحق له حذف هذه المشاركة؟».
3. IDOR: معرفة الرقم لا تعني الحق
GET /api/messages/1042 — إن غيّر المستخدم الرقم إلى 1043 هل يقرأ رسالة غيره؟ الحل: كل استعلام يقيد بالمالك أو يتحقق من العضوية:
const conv = await db.conversation.findFirst({
where: { id, participants: { some: { userId: viewer.userId } } },
});
if (!conv) throw notFound();
المعرفات العشوائية (cuid/uuid) تصعّب التخمين لكنها ليست بديلاً عن الفحص.
4. صلاحيات مقيدة بالنطاق
مشرف قسم «بايثون» يملك posts.moderate داخل قسمه فقط:
async function can(viewer: Viewer, perm: Permission, scope?: { nodeId?: string }) {
if (viewer.globalPermissions.has(perm)) return true;
if (!scope?.nodeId) return false;
const ancestors = await nodeAncestors(scope.nodeId); // صلاحية الأب تشمل الأقسام الفرعية
return viewer.scopedPermissions.some((s) => s.permission === perm && ancestors.includes(s.nodeId));
}
5. منع رفع الصلاحيات
- لا يمنح أحد ما لا يملكه: من يدير الأدوار لا يستطيع منح دور رتبته أعلى من رتبته أو مساوية لها، ولا صلاحية لا يملكها هو.
- لا يعدّل أحد من هو أعلى منه: مدير لا يحظر مالك المنصة.
- الحقول الحساسة لا تأتي من المستخدم:
roleوisAdminلا تُقبل أبداً من نموذج تعديل الملف الشخصي (استخدم مخطط تحقق صارماً يرفض الحقول غير المتوقعة). - أدوار الإدارة تتطلب التحقق بخطوتين، وكل تغيير في الأدوار يُسجل في سجل التدقيق.
6. الاختبارات
اكتب لكل عملية حساسة اختبارات «سلبية»:
it('عضو لا يستطيع حذف مشاركة عضو آخر', async () => {
const [a, b] = [await makeUser(), await makeUser()];
const post = await createPost(b);
await expect(deletePost(await actAs(a.id), post.id)).rejects.toMatchObject({ status: 404 });
expect(await db.post.findUnique({ where: { id: post.id } })).not.toBeNull();
});
تمرين عملي
- صمم ثلاثة أدوار (مشرف قسم، مشرف عام، مدير) وصلاحياتها في جدول.
- نفّذ
can()مع النطاق، واكتب اختبارات لمشرف قسم يحاول الإشراف على قسم آخر. - اكتب اختباراً يحاول فيه مدير منح نفسه دوراً أعلى ويتأكد من الرفض.
أخطاء شائعة
- الاعتماد على إخفاء الأزرار.
- فحص الدور بالاسم في كل مكان.
- فحص «هل يملك الصلاحية عموماً» دون ربطها بالمورد ونطاقه.
- قبول
roleأوownerIdمن جسم الطلب. - عدم تسجيل تغييرات الصلاحيات.
الخلاصة
صلاحيات صغيرة تجمعها أدوار، فحص على الخادم في كل عملية مرتبط بالمورد ونطاقه، قواعد تمنع منح ما لا تملكه، واختبارات سلبية لكل عملية حساسة.
مصادر مقترحة
- OWASP Top 10: A01 Broken Access Control.
- OWASP Cheat Sheet: «Authorization».
- NIST: نموذج RBAC (INCITS 359).