قامت منصة تعليمية تتيح للطلاب كتابة استعلامات MongoDB بإلغاء استخدام وحدة vm في Node.js، وأصبحت الآن تقوم بتحليل كل استعلام إلى شجرة بناء جملة مجردة (AST) قبل التنفيذ. يزيل هذا التغيير بيئة معزولة (sandbox) كان من الممكن اختراقها، مما يحمي الواجهة الخلفية (backend) من أي كود عشوائي قد يتم حقنه عبر سلسلة نصية مشوهة.

لماذا فشلت البيئة المعزولة الأصلية

قامت النسخة الأولى من التنفيذ بتغليف مقبض قاعدة بيانات نشط (live database handle) داخل استدعاء vm.runInContext وحاولت تقييد المستخدمين باستخدام تعبير نمطي (regular expression). كانت تسمح فقط بأسماء الدوال مثل find أو aggregate؛ وكان من المفترض تصفية أي معرف آخر.

هناك عيبان جعلا هذا النهج غير آمن:

  • يمكن تجاوز تصفية التعبيرات النمطية (Regex). تتيح لغة JavaScript للكود استخدام تدوين الأقواس (obj["constructor"]) للوصول إلى أي خاصية. يمكن للمهاجم استرداد منشئ الدالة Function وبناء دالة جديدة وتشغيل أي كود يختاره. لا يرى التعبير النمطي الوصول إلى الخاصية الأساسية لأن المصدر يمكن إعادة كتابته بطرق لا حصر لها.
  • وحدة vm ليست حدوداً أمنية. تنص وثائق Node على أن vm تعزل الكائن العام (global object) ولكن ليس العملية (process) بأكملها. من خلال حقن اتصال قاعدة بيانات نشط داخل البيئة المعزولة، احتفظ الكود الموجود داخل السياق بالقدرة على استدعاء أي دالة على ذلك الاتصال، بما في ذلك الدوال التي تكتب البيانات أو تحذفها. لم تمنع البيئة المعزولة الكود من التأثير على العملية المضيفة.

الحل القائم على AST

استبدل الفريق تنفيذ الكود بالتحليل الساكن (static analysis). أصبحت سلاسل الاستعلامات تُغذى الآن في محلل acorn الذي ينتج AST — وهو تمثيل شجري للبنية النحوية للكود. يتم فحص الـ AST عقدة تلو الأخرى مقابل قائمة بيضاء (whitelist) صارمة:

  • القيم الثابتة (Literals)، والمصفوفات، والكائنات مسموح بها فقط عندما تظهر كقيم مجردة.
  • استدعاءات الدوال تقتصر على مجموعة محددة مسبقاً (find, sort, limit, إلخ). ويتم رفض أي استدعاء آخر.
  • الوصول إلى الخصائص المحسوبة (مثل obj[expr]) أو أي نوع عقدة غير مدرج صراحةً يؤدي إلى فشل فوري.

نظرًا لأن المحلل يعمل على الشجرة وليس على النص الخام، فلا يمكن خداعه باستخدام تهجئات بديلة أو حيل تدوين الأقواس. سلسلة المنشئ (constructor chain) التي كانت ستمر عبر التعبير النمطي تظهر كعقدة غير معروفة ويتم رفضها قبل تشغيل أي كود.

ماذا يعني هذا للأمن

يتبع التصميم الجديد فلسفة "الرفض افتراضياً" (deny by default):

  • حدد ما هو مسموح به، وليس ما هو محظور. لا يمكن أن تكون القائمة البيضاء النصية شاملة؛ ولكن الـ AST لديه مجموعة محدودة من أنواع العقد، مما يجعل الفحص الشامل ممكناً.
  • لا تكشف أبداً عن الموارد النشطة داخل بيئة معزولة. إن تمرير مقبض قاعدة بيانات إلى سياق معزول يمنح الكود المعزول خطاً مباشراً إلى الواجهة الخلفية. نهج المحلل لا يسلم أبداً كائناً نشطاً لكود المستخدم؛ بل يستخرج فقط الغرض من الاستعلام.
  • تحقق من البنية، ثم نفذ بأمان. بمجرد اجتياز الـ AST لعملية التحقق، تقوم المنصة بترجمة الاستدعاءات المسموح بها إلى دوال محرك MongoDB الفعلية باستخدام مسار الكود الموثوق الخاص بها.

ما يجب مراقبته لاحقاً

  • راجع أي استخدام لـ eval أو new Function أو vm في قاعدة الكود الخاصة بك. حتى القائمة البيضاء يمكن تقويضها بسبب طبيعة JavaScript الديناميكية.
  • اعتمد تحليل AST للكود الذي ينشئه المستخدم حيثما أمكن ذلك. تجعل المكتبات مثل acorn أو esprima أو babel-parser عملية التحويل مباشرة وبسيطة.
  • قلل من تعرض الكائنات النشطة. إذا كان يجب الوصول إلى اتصال قاعدة بيانات، أو مقبض ملف، أو مقبس شبكة (network socket)، فقم بتغليفه في "وكيل" (proxy) لا يكشف إلا عن الحد الأدنى من الدوال التي تنوي السماح بها.
  • أتمت اختبار الحالات الحدية (edge cases). قم بإنشاء استعلامات تستخدم تدوين الأقواس، أو المفاتيح المحسوبة، أو التلاعب بالنماذج الأولية (prototype manipulation) للتحقق من أن المحلل الخاص بك يرفضها.

الخلاصة

إن الاعتماد على vm.runInContext كبيئة معزولة يعطي شعوراً زائفاً بالأمان؛ فمرشحات التعبيرات النمطية لا يمكنها تغطية بناء JavaScript المرن، والبيئة المعزولة لا تعزل موارد العملية. إن تحليل مدخلات المستخدم إلى AST ووضع قائمة بيضاء للعقد التي تفهمها فقط يوفر حاجزاً ملموساً وقابلاً للصيانة يوقف الكود الضار قبل تشغيله. إذا كانت منصتك تسمح للمستخدمين بكتابة الكود، فاستبدل التنفيذ بأسلوب eval بالتحليل الساكن اليوم.