التوثيق
/
وصفات عملية
/

تشويش VM مع eval وnew Function

تشويش VM مع eval وnew Function

كيف يتعامل تشويش VM مع بناء الكود الديناميكي (الاستدعاء المباشر لـ eval ومُنشئ Function)، وما الذي يُترجم إلى بايت كود وما الذي يُتخطى، والتحذيرات التي يصدرها المشوِّش، وكيفية تشخيص ReferenceError أثناء التشغيل.

لماذا يهمّك هذا

يترجم تشويش VM أجسام الدوال إلى بايت كود يُوزَّع عبر مفسّر داخل بيئة التشغيل. كما تُعاد تسمية المعرِّفات في النطاق المحيط. ويتفاعل التحويلان تفاعلًا سيئًا مع الكود المبني من نص أثناء التشغيل: eval(s) وnew Function(...s) وFunction(...s). فإذا أشار الكود المبني أثناء التشغيل إلى معرِّف أعاد المشوِّش تسميته، فستحصل على Uncaught ReferenceError: <renamed-name> is not defined في أول مرة تعمل فيها الدالة المولَّدة.

يتعامل المشوِّش مع كل نمط بشكل مختلف. والمصفوفة أدناه هي النسخة المختصرة؛ أما بقية هذه الصفحة فتشرح كل خانة.

ما يفعله المشوِّش، في لمحة

النمط في مصدركما يحدث
eval('literal string') (الجسم نص حرفي)يعمل بشكل صحيح. الدالة التي تحتوي هذا الاستدعاء تفقد ترجمة VM إلى بايت كود (فالاستدعاء المباشر لـ eval يقرأ المتغيرات المحلية المحيطة، وهي ما لا تحفظه الآلة الافتراضية بمجرد ترجمة الدالة إلى بايت كود).
eval(dynamicExpression)قد يتعطل أثناء التشغيل بخطأ ReferenceError. والدالة التي تحتوي هذا الاستدعاء — وكل دالة معرَّفة داخلها — تفقد أيضًا ترجمة VM إلى بايت كود.
(0, eval)(s) / window.eval(s) (غير مباشر)يعمل بشكل صحيح. والدالة التي تحتوي هذا الاستدعاء تُترجَم إلى بايت كود VM كالمعتاد. فالاستدعاء غير المباشر لـ eval لا يرى المتغيرات المحلية المحيطة، ولذلك لا يمكن لإعادة التسمية أن تعطّله.
new Function('a', 'b', 'return a + b') (كل الوسائط نصوص حرفية)يعمل بشكل صحيح. والدالة التي تحتوي هذا الاستدعاء تُترجَم إلى بايت كود VM كالمعتاد.
new Function(dynamicBody) / Function(dynamicBody)قد يتعطل أثناء التشغيل بخطأ ReferenceError. والدالة التي تحتوي هذا الاستدعاء — وكل دالة معرَّفة داخلها — تفقد أيضًا ترجمة VM إلى بايت كود.

تظهر كل هذه الأنماط أيضًا بوصفها تحذيرات غير قاتلة على نتيجة التشويش — انظر كشف المشكلة قبل التشغيل أدناه للاطلاع على أشكال التحذيرات ومقتطف CI.

لماذا يُعامَل الساكن والديناميكي معاملة مختلفة

يمكن لـ eval(s) أن يقرأ ويكتب المتغيرات المحلية من الدالة المستدعى فيها. فحين يكون s نصًا حرفيًا، يمكن للمشوِّش تحليل الجسم وقت التشويش وإعادة تسمية المعرِّفات بشكل متسق مع الكود المحيط. أما حين يكون s تعبيرًا ديناميكيًا، فلا يحدث التحليل إلا وقت التشغيل — وحينها تكون المعرِّفات قد أُعيدت تسميتها سلفًا، فيشير المصدر المبني وقت التشغيل إلى أسماء قديمة لم تعد موجودة.

يعمل new Function(s) بشكل مختلف: فالجسم يعمل دائمًا كأنه معرَّف في أعلى ملفك، مع وصول إلى المتغيرات العامة فقط، ولا يصل أبدًا إلى المتغيرات المحلية المحيطة بالاستدعاء. وهذا في حد ذاته آمن — لكن إن بنيت الجسم بربط معرِّف أُعيدت تسميته داخله (مثلًا عبر func.toString() لدالة أعاد المشوِّش كتابة داخلها)، فستصطدم الدالة المترجَمة وقت التشغيل بالنوع نفسه من ReferenceError.

أما new Function('return 42') الساكن فلا يحمل هذا الخطر أبدًا: فالجسم نص عادي لا يفحصه معيد التسمية، وهو لا يحتاج وقت التشغيل إلا إلى رؤية المتغيرات العامة. ويترك المشوِّش الاستدعاء في مكانه وتبقى الدالة المحيطة مؤهلة لترجمة VM إلى بايت كود.

الخطأ الذي تراه أثناء التشغيل

العَرَض الشائع هو ReferenceError في أول مرة تعمل فيها الدالة المبنية ديناميكيًا:

browser console

خطأ

TU هنا معرِّف أعاد المشوِّش تسميته وأدخله داخل نطاق تعبير IIFE للحزمة. ويقيّم الاستدعاء الديناميكي لـ eval / مُنشئ Function جسمًا يشير إليه، لكن الجسم يعمل في نطاق يكون فيه TU غير معرَّف.

كشف المشكلة قبل التشغيل

يصدر المشوِّش تحذيرات غير قاتلة عبر الواجهة البرمجية لتتمكن من التقاط هذه الأنماط في CI قبل الشحن. ويهمّك نوعا تحذير اثنان:

  • DynamicCodeRenameRisk — دالة تحتوي على استدعاء eval / new Function / Function ديناميكي يُبنى جسمه وقت التشغيل.
  • VMDynamicCodeSkipped — تُخطِّي ترجمة VM إلى بايت كود لدالة بسبب أحد الأنماط أعلاه. ويتضمن اسم الدالة (إن توفّر) ونوع البنية التي استدعت التخطي.

ci-build.mjs

JavaScript

إذا كان نوع تحذير متوقعًا في بنائك وتفضّل إسكاته عند المصدر بدلًا من ترشيحه في CI، فإن الخيار warnings (الإصدار 7.8.0 فما فوق) يتحكم فيما تصدره getWarnings(): فـ 'none' تكبح كل شيء، وخريطة لكل نوع مثل { VMDynamicCodeSkipped: false } تكتم نوعًا واحدًا فقط مع إبقاء البقية.

حلول بديلة

  • بدّل إلى الاستدعاء غير المباشر لـ eval ((0, eval)(s))

    مفيد فقط لحالة eval. فالاستدعاء غير المباشر لـ eval يعمل في النطاق العام، ولذلك لا يرى المتغيرات المحلية المحيطة — ولهذا السبب نفسه لا يمكنه الإشارة إلى المعرِّفات المعاد تسميتها أيضًا. وتبقى الدالة التي تحتوي الاستدعاء مترجَمة إلى بايت كود VM.

  • اجعل الجسم ساكنًا بالكامل

    بالنسبة إلى new Function، إن استطعت التعبير عن الجسم بنص حرفي واحد / نص قالب واحد دون أي إقحام، فلا يحمل الاستدعاء خطر إعادة تسمية وتبقى الدالة المحيطة به مترجَمة إلى بايت كود. فـ new Function('a', 'b', 'return a + b') سليم؛ أما new Function('return ' + expr) فلا.

  • انقل الاستدعاء إلى دالته الخاصة على المستوى الأعلى، وفك تغليف IIFE

    تُفحَص كل دالة على المستوى الأعلى بشكل مستقل. فرفع استدعاء بناء الكود الديناميكي إلى دالته الخاصة على المستوى الأعلى يعني أن تلك الدالة وحدها تفقد ترجمة VM إلى بايت كود، بدلًا من تتالي التخطي عبر تعبير IIFE على المستوى الأعلى يغلّف حزمتك بأكملها.

  • بدّل إلى vmTargetFunctionsMode: 'comment'

    وضع اختياري: لا تُترجَم إلى بايت كود إلا الدوال المؤشَّر عليها بـ /* javascript-obfuscator:vm */. تخطَّ التأشير على الدالة التي تحتوي استدعاء الكود الديناميكي، وترجِم البقية إلى بايت كود. انظر استهداف الدوال.

  • تجاوز التخطي بـ vmForceCompileDynamicCode: true (الإصدار 6.14.0 فما فوق)

    مخرج الملاذ الأخير. فعند تفعيله، يترجم المشوِّش الدالة المحيطة إلى بايت كود على أي حال ويكبح تحذير VMDynamicCodeSkipped. استخدمه فقط حين تضمن أن الجسم المبني وقت التشغيل لا يشير أبدًا إلى معرِّف يعيد المشوِّش تسميته — وإلا فأنت تقايض تشويشًا نظيفًا بـ ReferenceError أثناء التشغيل. ولا يزال DynamicCodeRenameRisk يُطلَق ليتمكن CI من مواصلة الحجب بناءً عليه. في لوحة التحكم، هذا هو مفتاح "Force Compile Dynamic Code" ضمن مجموعة Overrides في قسم VM.

صفحات ذات صلة