- الأمان
- OWASP
- التحكم بالوصول
IDOR وخلل التحكم بالوصول: دليل عملي
كيف يتسلل IDOR وخلل التحكم بالوصول إلى REST وGraphQL ومعالجات المسارات في Next.js، وكيف تختبرها، وما الإصلاحات التي تصمد في قواعد الكود الحقيقية.
· قراءة في 7 دقائق · Lina Source LLC
خلل التحكم بالوصول هو فئة الأخطاء التي تنجو من كل ترقية لإطار العمل. أداة ORM لديك تهرّب استعلامات SQL، ومحرك القوالب يهرّب HTML، لكن لا شيء في الحزمة التقنية يعرف أن الفاتورة 4812 تخص «أليس» لا «بوب». هذه المعرفة تعيش في كودك أنت، وحين ينسى معالج واحد تطبيقها، يستطيع أي مستخدم مسجّل الدخول قراءة بيانات غيره أو تعديلها.
الشكل الأكثر شيوعًا هو المرجع المباشر غير الآمن للكائنات، أو IDOR: يرسل العميل معرّفًا، فيحمّل الخادم السجل المطابق له، ولا يتحقق أحد مما إذا كان المستدعي مخوّلًا برؤيته. استغلاله لا يحتاج إلى أدوات خاصة؛ يكفي متصفح وحساب ثانٍ ورقم مُغيَّر في عنوان URL. ونادرًا ما تكتشفه الماسحات التي تبحث عن استدعاءات دوال خطرة، لأن الكود المصاب لا يحتوي على شيء خطر: مجرد استعلام عادي تمامًا لقاعدة البيانات ينقصه شرط واحد.
معرّفات CWE الثلاثة التي ستصادفها
- CWE-639، تجاوز التفويض عبر مفتاح يتحكم فيه المستخدم: هذا هو IDOR الكلاسيكي. يُختار السجل بمعرّف يتحكم فيه المهاجم، ولا يُتحقق من الملكية أبدًا.
- CWE-862، غياب التفويض: لا يجري المعالج أي تحقق من التفويض إطلاقًا. وغالبًا ما يكون ذلك نقطة نهاية إدارية أو داخلية افتُرض أنه لا يمكن الوصول إليها.
- CWE-285، التفويض غير السليم: يوجد تحقق لكنه خاطئ. فهو يفحص الحقل الخطأ، أو يتحقق من صلاحية القراءة في عملية كتابة، أو يثق بدور يرسله العميل.
هذا التمييز مهم عند إصلاح الخلل. غياب التحقق يعني إضافة تحقق، أما التحقق غير السليم فيعني أن تصوّرك لمن يحق له فعل ماذا خاطئ من أساسه، ومن المرجّح أن الخطأ نفسه متكرر في مواضع أخرى. حين تجد أيًّا منهما، ابحث عن نظائره قبل إغلاق التذكرة. نادرًا ما تكون أخطاء التحكم بالوصول حالات منفردة؛ فهي تتبع الأنماط التي ينسخها الفريق من معالج إلى آخر.
كيف يحدث ذلك في معالج مسار Next.js
إليك النمط في أكثر أشكاله شيوعًا. يصادق المعالج على المستخدم، وهو ما يعطي إحساسًا بالأمان، ثم يحمّل السجل بمعرّفه وحده. فحص الجلسة يجيب عن سؤال من المتصل، ولا شيء يجيب عن سؤال ما إذا كان هذا المتصل يحق له رؤية هذه الفاتورة.
@@CODE@@الإصلاح هو جعل الملكية جزءًا من الاستعلام نفسه، لا خطوة منفصلة يمكن نسيانها. إذا لم يكن السجل ملكًا للمتصل، فلن تعيد قاعدة البيانات شيئًا وسيجيب المعالج بـ 404.
@@CODE@@إعادة 404 بدلًا من 403 أمر مقصود. فالرمز 403 يؤكد وجود السجل، ما يتيح للمهاجم حصر المعرّفات الصالحة حتى لو لم يستطع قراءتها. والأمر نفسه ينطبق على التوقيت ورسائل الخطأ: يجب ألا يمكن تمييز الاستجابة لسجل يخص شخصًا آخر عن الاستجابة لسجل لم يوجد قط.
REST: نقاط النهاية التي ينساها الناس
عادةً ما تحمي الفرق طلب GET الواضح حسب المعرّف. أما الأخطاء فتختبئ في الأفعال الأخرى وفي أطراف الواجهة البرمجية:
- معالجات PATCH وDELETE التي نُسخت من معالج GET قبل إضافة التحقق من الملكية.
- المسارات المتداخلة مثل /projects/:projectId/tasks/:taskId، حيث يُتحقق من المشروع لكن المهمة تُحمَّل بـ taskId وحده وقد تنتمي إلى مشروع آخر.
- نقاط النهاية المجمّعة التي تقبل مصفوفة من المعرّفات وتتحقق من أولها فقط.
- تنزيلات الملفات ومهام التصدير، التي تمر غالبًا عبر خدمة منفصلة لها تحقق خاص بها أضعف.
- حمولات التحديث التي تقبل ownerId أو organizationId أو role من جسم الطلب وتكتبها مباشرة في قاعدة البيانات (الإسناد الجماعي).
GraphQL يوسّع سطح الهجوم
في GraphQL، يمكن الوصول إلى الكائن نفسه عبر مسارات كثيرة. التحقق على مستوى الاستعلام invoice(id) لا يفيد إذا كانت الفاتورة نفسها قابلة للوصول أيضًا عبر customer { invoices } أو عبر بحث node(id) أو عبر نوع الإرجاع في mutation. كل resolver يعيد كائنًا هو نقطة دخول. وتضيف طبقات التجميع مثل DataLoader فخًا آخر: فالمحمّل المفهرس بالمعرّف وحده سيعيد السجلات لأي مشاهد دون تردد، ويمكن لذاكرته المؤقتة أن تقدّم بيانات مستخدم إلى طلب لاحق إذا كانت مشتركة بين الطلبات.
النهج الموثوق هو إجراء التفويض في طبقة البيانات التي تستدعيها الـ resolvers، لا داخل الـ resolvers نفسها. إذا مرّ كل مسار إلى الفاتورة عبر دالة واحدة تأخذ المشاهد وتقيّد الاستعلام بنطاقه، فلن يتمكن حقل أو علاقة جديدة من تجاوزها. وتحقق أيضًا من مدخلات الـ mutations: فحقل مثل ownerId في نوع إدخال دعوة صريحة لإعادة إسناد السجلات. وأخيرًا، تذكّر أن الاستبطان (introspection) ورسائل الخطأ تكشف مخططك، لذا افترض أن المهاجمين يعرفون كل حقل وعلاقة تعرضها.
الخلل نفسه في Python
الشكل مطابق في FastAPI مع SQLAlchemy. النسخة المصابة تستدعي db.get(Document, doc_id)؛ والنسخة المُصلحة ترشّح حسب المالك في العبارة نفسها.
@@CODE@@إصلاحات تصمد
قيّد كل استعلام بالمالك أو المستأجر
ضع معرّف المستخدم أو المؤسسة في عبارة WHERE لكل عملية قراءة وكتابة. هذا يحوّل التفويض إلى خاصية من خصائص الاستعلام، يسهل رؤيتها أثناء المراجعة. وفي التطبيقات متعددة المستأجرين، يمكن لأمان مستوى الصفوف (row-level security) في Postgres فرض حدود المستأجر كطبقة ثانية، فيعيد المرشّح المنسي لا شيء بدلًا من صفوف عميل آخر. وينطبق التقييد نفسه على عمليات الكتابة. يجب أن يكون التحديث عبارة واحدة مرشّحة بالمعرّف والمالك معًا، مثل updateMany بالشرطين يليه تحقق من أن صفًا واحدًا بالضبط قد تغيّر، بدلًا من قراءة ثم تحقق ثم كتابة منفصلة قد تتعرض لحالة تسابق.
اجعل القرار مركزيًا
عبارات if المبعثرة تتباعد مع الوقت. مجموعة صغيرة من الدوال المساعدة، واحدة لكل مورد، تُبقي القاعدة في مكان واحد وتجعل أي معالج لا يستدعي دالة مساعدة يبرز بوضوح.
@@CODE@@الرفض افتراضيًا
الأدوار غير المعروفة والعضويات المفقودة والإجراءات غير المتوقعة يجب أن تنتهي جميعها إلى الرفض. في أطر العمل التي تدعم الـ middleware، اشترط المصادقة لكل شيء وحدّد المسارات العامة صراحةً، لا العكس. يجب أن يكون المسار الجديد مغلقًا إلى أن يقرر أحدهم خلاف ذلك. ولا تأخذ الدور أو المستأجر أو معرّف المستخدم من جسم الطلب أو من ترويسة يضبطها العميل أبدًا؛ بل استخرجها من الجلسة الموثّقة على الخادم في كل مرة.
المعرّفات العشوائية مثل UUID تستحق الاستخدام، لكنها ليست إصلاحًا. فالمعرّفات تتسرب عبر عناوين URL والسجلات والروابط المشتركة وترويسات referrer. اعتبرها غير قابلة للتخمين فقط بمعنى أنها تبطئ الحصر، ولا تعتمد عليها أبدًا كفحص للوصول.
كيف تختبر ذلك
اختبار IDOR بسيط ومتكرر، ولهذا يستحق الأتمتة بعد أن تجريه يدويًا مرة. ابدأ بجرد: اذكر كل مسار وresolver ومهمة خلفية تقبل معرّفًا، بما في ذلك المعرّفات المخفية في أجسام الطلبات وسلاسل الاستعلام والترويسات.
- أنشئ حسابين، A وB، ويُفضَّل أن يكونا في مؤسستين منفصلتين. أنشئ سجلًا بالحساب A ودوّن معرّفه.
- أعد إرسال كل طلب يشير إلى ذلك المعرّف بجلسة الحساب B: طلبات GET وPATCH وDELETE والتنزيلات والتصديرات وأي query أو mutation في GraphQL يتعامل معه.
- توقّع 404 لها جميعًا. أي استجابة 200، وأي 403 تؤكد الوجود، هي اكتشاف أمني.
- حوّل الفحص اليدوي إلى اختبار تكامل لكل مورد، حتى يفشل في CI أي معالج جديد لا يستخدم استعلامًا مقيّدًا.
- ابحث بـ grep عن عمليات البحث بالمفتاح الأساسي وحده، مثل findUnique({ where: { id } }) أو db.get(Model, id)، وبرّر كل واحدة منها.
مراجعة الكود تلتقط ما تفوّته الاختبارات، لأن التحقق الغائب ظاهر في الكود المصدري حتى لو لم يكتب أحد اختبارًا لذلك المسار. يقرأ CodeAuditAgent مستودع GitHub عامًا أو مقتطفًا ملصوقًا ويبلّغ عن ثغرات التحكم بالوصول مع معرّف CWE والسطر المقتبس وسيناريو استغلال وتصحيح مقترح، وهي طريقة سريعة للحصول على مراجعة ثانية لكل المعالجات دفعة واحدة.
قائمة تحقق قصيرة
- كل استعلام يأخذ معرّفًا مقدَّمًا من العميل يرشّح أيضًا حسب مستخدم المتصل أو مستأجره.
- التفويض موجود في دوال مساعدة مشتركة أو في طبقة البيانات، لا في عبارات if منسوخة ومُلصقة.
- الحالات غير المعروفة تُرفض؛ والمسارات العامة هي الاستثناء الصريح.
- عمليات الكتابة تُفحص بالعناية نفسها التي تُفحص بها القراءة، بما في ذلك المسارات المجمّعة والمتداخلة.
- توجد اختبارات بحسابين لكل مورد وتعمل في CI.