ثغرات BOLA في واجهات برمجة تطبيقات GraphQL: التهديد الصامت
ثغرات BOLA في واجهات برمجة تطبيقات GraphQL: التهديد الصامت
تظل ثغرة تفويض مستوى كائن مكسور (Broken Object Level Authorization - BOLA) أكثر الثغرات الأمنية حرجاً والتي تؤثر على تطبيقات الويب الحديثة. عندما نهاجر إلى البنيات المعمارية القائمة على الرسوم البيانية (graph-based architectures)، فإن مساحة الهجوم لا تتغير فحسب، بل تتسع بطرق غير متوقعة. يستكشف هذا المنشور ثغرات BOLA في واجهات برمجة تطبيقات GraphQL، ويشرح بدقة لماذا تفشل عقليات التفويض التقليدية المتمحورة حول REST بشكل كارثي عند تطبيقها على الرسوم البيانية. سنقوم بتفكيك المشكلة، وتحليل سبب صعوبة حلها، واقتراح بنية مرنة، وتقديم تطبيق ملموس، ومناقشة الفخاخ التي ستواجهها حتماً.
إذا كنت تتعامل مع GraphQL كأنها نقطة نهاية REST محسنة، فإن واجهة البرمجة (API) الخاصة بك معرضة للخطر بالفعل. لقد حان الوقت لإصلاح طبقة التفويض الخاصة بك.
المشكلة: التفويض على مستوى الكائن مكسور
في بنية REST التقليدية، يكون التفويض مباشراً نسبياً لأن طوبولوجيا نقطة النهاية تترجم بوضوح إلى طوبولوجيا البيانات. أنت تطلب GET /api/users/123/financials. يعترض الموجه الطلب، ويشغل البرمجيات الوسيطة (middleware) التي تتحقق مما إذا كان المستخدم المصادق عليه لديه الإذن للوصول إلى البيانات المالية للمستخدم 123، ويسمح بالطلب أو يرفضه. الحدود ثابتة، ومحددة جيداً، وسهلة الفحص.
يدمر GraphQL هذه البساطة.
مع GraphQL، يحدد العميل شكل الاستجابة. نقطة النهاية هي دائماً /graphql، ويحتوي جسم الطلب على استعلام يمكنه اجتياز كيانات وعلاقات وأعماق متعددة. يمكن لاستعلام واحد الوصول إلى الملف الشخصي للمستخدم وأصدقائه ومنشورات أصدقائه والتعليقات على تلك المنشورات.
query BOLAExploit {
user(id: "current-user-id") {
name
organization {
id
members(first: 100) {
id
email
salary # Vulnerable field
}
}
}
}
في المثال أعلاه، يبدأ المهاجم من كائن يتحكم فيه (user)، وينتقل إلى كائن مشترك (organization)، ثم يطلب حقلاً حساساً (salary) لجميع أعضاء تلك المؤسسة. إذا كان منطق التفويض يتحقق فقط مما إذا كان المستخدم ينتمي إلى المؤسسة ولكنه يفشل في التحقق مما إذا كان مسموحاً للمستخدم بقراءة حقل الـ salary للأعضاء الآخرين، فهذا يعني وجود ثغرة BOLA.
المشكلة الأساسية هي أن محللات GraphQL (GraphQL resolvers) تنفذ بشكل مستقل. يعرف محلل الـ salary أنه بحاجة إلى إرجاع قيمة عشرية لمستخدم معين، ولكنه غالباً ما يفتقر إلى سياق من يطلب وكيف وصل إلى هذه العقدة. يعد فقدان السياق هذا هو التربة الخصبة لثغرات BOLA.
لماذا يصعب اكتشاف ثغرات BOLA في واجهات برمجة تطبيقات GraphQL
يصعب للغاية تحديد ثغرات BOLA في واجهات برمجة تطبيقات GraphQL من خلال الفحص الآلي أو مراجعة الكود العادية. وإليك السبب:
1. العمق واجتياز الرسم البياني (Graph Traversal)
لا يستعلم المهاجمون عن العقد الجذرية (root nodes) فحسب؛ بل يجتازون الرسم البياني لتجاوز فحوصات مستوى الجذر. قد يكون استعلام الجذر لـ users(id: "123") محمياً، ولكن ماذا عن post(id: "456") { author { id email } }؟ إذا لم يقم محلل author بإجراء نفس فحوصات التفويض مثل استعلام الجذر لـ user، فسيتم كشف البيانات. الرسم البياني مترابط، مما يعني أن هناك عشرات المسارات لنفس عقدة البيانات. وتأمين مسار واحد لا فائدة منه إذا ظلت المسارات الأخرى مفتوحة.
2. تجزئة السياق (Context Fragmentation)
في العديد من أطر العمل (مثل Apollo Server أو Yoga)، تكون المحللات عبارة عن وظائف نقية تستقبل parent و args و context و info. يضع المطورون بشكل متكرر منطق التفويض في إعداد context (مثل استخراج معرف المستخدم من JWT)، لكنهم يفشلون في تمرير سياق عمل كافٍ إلى المحللات على مستوى الحقل. بحلول الوقت الذي يصل فيه التنفيذ إلى حقل متداخل بعمق، لا يملك المحلل سوى الكائن الأصل ومعرف المستخدم. وهو يفتقر إلى قواعد العمل المعقدة المطلوبة لاتخاذ قرار تفويض دقيق.
3. الوضع الافتراضي "الفشل المفتوح" (Fail-Open)
ستقوم أطر عمل GraphQL، افتراضياً، بتنفيذ أي محلل إذا سمح المخطط بذلك. وما لم تكتب كوداً صريحاً لرفض الوصول، فسيتم السماح بالطلب. هذا نموذج "الفشل المفتوح" خطير بطيعته. يجب أن تفشل الأنظمة الآمنة في حالة الإغلاق (fail-closed). وإذا كانت سياسة التفويض مفقودة، يجب رفض الطلب.
4. الإفراط في جلب البيانات والتمويه (Over-fetching and Masking)
في REST، يطلب المهاجم نقطة نهاية محددة، مما يجعل نيته واضحة في السجلات. أما في GraphQL، فيمكن للمهاجم دفن استغلال BOLA عميقاً داخل استعلام يبدو شرعياً يجلب عشرات الحقول الآمنة. يتم تمويه الحمولة الخبيثة بواسطة الضوضاء، مما يجعل أنظمة كشف التسلل (IDS) وجدران حماية تطبيقات الويب (WAF) عمياء تماماً عن الهجوم.
حلول معمارية لتفويض GraphQL
للقضاء على BOLA، يجب أن نبتعد عن فحوصات التفويض المخصصة والمنشورة في جميع أنحاء المحللات لدينا. نحن بحاجة إلى بنية تفويض مركزية، وحتمية، وتفشل مغلقة (fail-closed).
نهج محرك السياسات (Policy Engine)
تفصل البنية الأكثر مرونة منطق التفويض عن محللات GraphQL تماماً. لا ينبغي أن تعرف المحللات كيف تقيم الأذونات؛ يجب أن تعرف فقط من تسأل. وهذا يعني تقديم محرك سياسات مخصص.
يعمل محرك السياسات كمصدر وحيد للحقيقة لجميع قرارات التفويض. وهو يأخذ ثلاثة مدخلات:
- الفاعل (Actor): المستخدم المصادق عليه (مثل معرف المستخدم، الأدوار، الصفات).
- الإجراء (Action): العملية التي يتم إجراؤها (مثل
read،update،delete). - المورد (Resource): الكائن المحدد الذي يتم الوصول إليه (مثل معرف المستند، معرف المؤسسة).
عندما يحتاج المحلل إلى إرجاع البيانات، فإنه يستعلم من محرك السياسات: "هل يمكن للفاعل X تنفيذ الإجراء Y على المورد Z؟"
التحكم في الوصول المستند إلى الصفات (ABAC)
التحكم في الوصول المستند إلى الأدوار (RBAC) غير كافٍ لمنع BOLA. تتعلق BOLA بشكل أساسي بالوصول على مستوى الكائن. إن معرفة أن المستخدم هو "مسؤول" (Admin) لا فائدة منها إذا كان يحاول الوصول إلى مستند في مستأجر (tenant) مختلف.
يجب استخدام التحكم في الوصول المستند إلى الصفات (Attribute-Based Access Control - ABAC). يقيم ABAC السياسات بناءً على صفات الفاعل والمورد والبيئة.
على سبيل المثال، قد تنص السياسة على ما يلي: "يمكن للمستخدم قراءة مستند إذا كان organizationId للمستخدم يطابق organizationId للمستند، وكان المستند معلماً بأنه published (منشور)."
توجيهات المخطط (Schema Directives)
لفرض السياسات بشكل نظيف، يمكننا استخدام توجيهات مخطط GraphQL (GraphQL Schema Directives). تسمح لنا التوجيهات بإرفاق بيانات تعريفية تصريحية بالمخطط الخاص بنا. ويمكننا تحديد توجيه @auth يحدد الأذونات المطلوبة لحقل أو كائن.
directive @auth(action: String!) on FIELD_DEFINITION | OBJECT
type User @auth(action: "read:user") {
id: ID!
email: String! @auth(action: "read:user_email")
salary: Float @auth(action: "read:user_salary")
}
يضمن هذا النهج التصريحي أن تكون قواعد التفويض مرئية، وموثقة، ومفروضة باستساق قبل تنفيذ المحلل.
تطبيق: كتابة محللات آمنة
دعونا نبني تطبيقاً آمناً باستخدام Node.js، و Apollo Server v4، ومحرك سياسات ABAC مخصص.
الخطوة 1: محرك السياسات
أولاً، نحدد محرك سياسات بسيطاً يقيم القواعد بناءً على المستخدم والمورد.
// policyEngine.ts
type User = { id: string; role: string; organizationId: string };
type Resource = { __typename: string; id: string; organizationId?: string; ownerId?: string };
export class PolicyEngine {
can(user: User, action: string, resource: Resource): boolean {
if (user.role === 'SUPERADMIN') return true;
switch (resource.__typename) {
case 'Document':
return this.checkDocumentAccess(user, action, resource);
case 'User':
return this.checkUserAccess(user, action, resource);
default:
return false; // Fail-closed
}
}
private checkDocumentAccess(user: User, action: string, doc: Resource): boolean {
if (action === 'read') {
return user.organizationId === doc.organizationId;
}
if (action === 'update' || action === 'delete') {
return user.id === doc.ownerId;
}
return false;
}
private checkUserAccess(user: User, action: string, targetUser: Resource): boolean {
if (action === 'read:salary') {
// Users can only read their own salary, unless they are HR
return user.id === targetUser.id || user.role === 'HR';
}
if (action === 'read') {
return user.organizationId === targetUser.organizationId;
}
return false;
}
}
الخطوة 2: تهيئة السياق (Context)
نقوم بإنشاء مثيل لمحرك السياسات وحقنه في سياق GraphQL إلى جانب المستخدم المصادق عليه.
// context.ts
import { PolicyEngine } from './policyEngine';
export interface GraphQLContext {
user: User;
policyEngine: PolicyEngine;
}
export const context = async ({ req }): Promise<GraphQLContext> => {
const user = authenticateRequest(req); // e.g., verify JWT
return {
user,
policyEngine: new PolicyEngine(),
};
};
الخطوة 3: المحللات الآمنة
الآن، داخل المحللات الخاصة بنا، نستخدم محرك السياسات لفرض تفويض على مستوى الكائن قبل إرجاع البيانات.
// resolvers.ts
export const resolvers = {
Query: {
document: async (_, { id }, context: GraphQLContext) => {
const doc = await database.getDocument(id);
if (!doc) return null;
// Object-Level Authorization Check
if (!context.policyEngine.can(context.user, 'read', { __typename: 'Document', ...doc })) {
throw new Error('Unauthorized');
}
return doc;
},
},
User: {
salary: (parent, _, context: GraphQLContext) => {
// parent is the User object being resolved
if (!context.policyEngine.can(context.user, 'read:salary', { __typename: 'User', ...parent })) {
throw new Error('Unauthorized to view salary');
}
return parent.salary;
}
}
};
من خلال دفع منطق التفويض إلى محرك سياسات مخصص والتحقق من الوصول بوضوح بعد جلب الكائن ولكن قبل إرجاعه، نقوم بالقضاء على ثغرة BOLA.
الفخاخ الشائعة والحالات الخاصة
حتى مع وجود بنية قوية، هناك فخاخ يجب تجنبها.
1. مشكلة تفويض N+1 (The N+1 Authorization Problem)
إذا قمت بالاستعلام عن قائمة تضم 100 مستند، وتحقق المحلل الخاص بك من التفويض لكل مستند على حدة، فقد تطلق 100 استعلام لقاعدة البيانات لمجرد جلب الصفات اللازمة لمحرك السياسات. هذه هي مشكلة N+1 المطبقة على التفويض.
الحل: استخدم DataLoaders لتجميع جلب بيانات تعريف التفويض. والأفضل من ذلك، ادفع فلاتر التفويض إلى طبقة قاعدة البيانات. بدلاً من جلب 100 مستند وتصفيتها في Node.js، قم بتعديل استعلام SQL لإرجاع المستندات التي يُسمح للمستخدم برؤيتها فقط: SELECT * FROM documents WHERE organization_id = ?.
2. تسريب البيانات من خلال الأخطاء (Leaking Data Through Errors)
عندما يحاول مستخدم الوصول إلى مورد لا ينبغي له الوصول إليه، فإن إرجاع خطأ مثل Unauthorized to access document 123 يؤكد وجود المستند 123. هذا تسريب للمعلومات.
الحل: إذا حاول مهاجم الوصول إلى كائن لا يملك رؤية له، يجب أن تعيد واجهة البرمجة (API) استجابة عامة مثل Not Found أو null، تماماً كما لو كان الكائن غير موجود. لا ترجع Unauthorized إلا إذا كان المستخدم يعلم بوجود الكائن ولكنه يفتقر إلى الإذن المحدد لتنفيذ الإجراء.
3. تجاهل الطفرات (Mutations)
غالباً ما يؤمن المطورون استعلاماتهم (Queries) بشكل مكثف ولكنهم يهملون الطفرات (Mutations). وتعد ثغرة BOLA في طفرة updateUser أكثر خطورة من ثغرة في استعلام user.
الحل: تتطلب الطفرات نفس فحوصات ABAC، إن لم تكن أكثر صرامة. تحقق من أن المستخدم لديه الإذن لتحديث الكائن المحدد قبل تنفيذ الكتابة في قاعدة البيانات.
4. افتراضات حالة جانب العميل (Client-Side State Assumptions)
لا تثق أبداً في العميل لتقديم سياق التفويض. إذا كانت الطفرة الخاصة بك تقبل المدخل input: { id: "123", organizationId: "456", newData: "..." }، فلا يمكنك الوثوق في أن المستخدم ينتمي بالفعل إلى organizationId "456". يجب جلب الكائن من قاعدة البيانات والتحقق من حالته الحقيقية مقابل رمز المستخدم المصادق عليه.
النتيجة: طبقة GraphQL محصنة
إن معالجة ثغرات BOLA في واجهات برمجة تطبيقات GraphQL ليست ميزة يمكنك إضافتها في نهاية دورة العمل السريعة (sprint). بل تتطلب تحولاً أساسياً في كيفية تصميم تطبيقك. من خلال التخلي عن فحوصات المحللات المخصصة لصالح محرك سياسات مركزي، واعتماد ABAC، وفرض نموذج الفشل المغلق (fail-closed)، فإنك تحول واجهة برمجة تطبيقات GraphQL من مساحة هجوم ضخمة إلى واجهة محصنة وقابلة للتنبؤ.
توقف عن الاعتماد على الغموض. وتوقف عن الثقة في جانب العميل. افترض أن كل استعلام هو استعلام معادٍ، وقيم كل تنفيذ عقدة مقابل سياسات صارمة، وابنِ واجهة برمجة تطبيقات تدافع عن نفسها. إن تكلفة تنفيذ تفويض قوي على مستوى الكائن مرتفعة، لكن تكلفة خرق BOLA كارثية. اكتب محللات آمنة.
خدمة سفن لابس
اختبار الاختراق VAPT والأمن السيبراني
