تشويش 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. |
يكتشف المترجِم أيضًا eval?.(code) بحذر ويعامله معاملة eval المباشر. تعرّف JavaScript صيغة الاستدعاء
الاختياري هذه على أنها eval غير مباشر؛ واكتشاف المترجِم لها لا يغيّر سلوك اللغة هذا.
لا تُنتج تحذيرات غير قاتلة إلا بعض هذه الأنماط، وفقط عندما يقع الاستدعاء داخل دالة: فاستدعاء eval أو new Function
أو Function الديناميكي يُبلغ عن DynamicCodeRenameRisk وVMDynamicCodeSkipped كليهما، واستدعاء eval('...') الثابت
لا يُبلغ إلا عن VMDynamicCodeSkipped، أما eval غير المباشر وnew Function الثابت بالكامل فلا يُبلغان عن شيء. والاستدعاء
الديناميكي على المستوى الأعلى من الملف، خارج أي دالة، لا يُبلَغ عنه أبدًا. انظر
اكتشاف المشكلة قبل وقت التشغيل أدناه لأنواع التحذيرات ومقتطف لـ CI.
يمتد التخطي عبر دوال IIFE. إذا كانت دالة IIFE في المستوى الأعلى تغلّف حزمتك بالكامل وكانت أي دالة بداخلها تستخدم
eval أو new Function ديناميكيًا، فستُستبعد دالة IIFE بأكملها من التحويل إلى بايت كود VM. انظر
سلوك eval المباشر لنمط فك تغليف IIFE الذي يحدّ من نطاق
الضرر.
لماذا يُعامَل الثابت والديناميكي بشكل مختلف
يستطيع eval(s) قراءة المتغيرات المحلية وكتابتها في الدالة التي يُستدعى منها. فعندما يكون s نصًا حرفيًا،
يمكن للمشوِّش تحليل الجسم وقت التشويش وإعادة تسمية المعرِّفات بما يتسق مع الكود المحيط. أما عندما يكون
s تعبيرًا ديناميكيًا، فلا يحدث التحليل إلا أثناء التشغيل، وحينها تكون المعرِّفات قد أُعيدت
تسميتها بالفعل، فيشير المصدر المبني أثناء التشغيل إلى أسماء قديمة لم تعد موجودة.
يعمل new Function(s) وeval غير المباشر بطريقة مختلفة: إذ يُنفَّذ الجسم دائمًا في النطاق العام، ولا يصل إلا إلى
المتغيرات العامة ولا يصل أبدًا إلى المتغيرات المحلية المحيطة بالاستدعاء. فلا يمكن أن يعتمدا على متغيرات محلية مُعاد تسميتها، لكنهما قد يفشلان إذا
أشارا إلى متغيرات عامة أُعيدت تسميتها أو أُزيلت، أو إذا بنيت الجسم بدمج معرِّف مُعاد تسميته
فيه (مثلًا عبر func.toString() لدالة أعاد المشوِّش كتابة محتوياتها الداخلية).
أما الجسم الثابت الذي لا يستخدم سوى معاملاته الخاصة، مثل new Function('a', 'b', 'return a + b')، فلا يحمل هذا
الاعتماد: فالجسم نص عادي لا تفحصه أداة إعادة التسمية أبدًا. يترك المشوِّش الاستدعاء في مكانه وتبقى
الدالة المحيطة مؤهلة للتحويل إلى بايت كود VM. لكن تغيير صيغة eval وحده لا يجعل أي كود
ديناميكي آمنًا؛ راجع التحذيرات واختبر الحزمة النهائية.
الخطأ الذي تراه أثناء التشغيل
العَرَض الشائع هو ReferenceError في أول مرة تعمل فيها الدالة المبنية ديناميكيًا:
TU هنا معرِّف مُعاد تسميته أدخله المشوِّش داخل نطاق IIFE الخاص بالحزمة. ويُقيِّم استدعاء eval
الديناميكي أو مُنشئ Function جسمًا يشير إليه، لكن هذا الجسم يعمل في نطاق يكون فيه TU غير معرَّف.
اكتشاف المشكلة قبل وقت التشغيل
يُصدر المشوِّش تحذيرات غير قاتلة عبر API لتتمكن من التقاط بعض هذه الأنماط في CI قبل النشر. ويعنينا هنا نوعان من التحذيرات، ولا يُبلَغ عن أيٍّ منهما إلا للاستدعاءات الواقعة داخل دالة:
DynamicCodeRenameRisk: تبني دالة كودًا من نص أثناء التشغيل: استدعاء مباشر لـevalأو استدعاءnew Function/Functionجسمه غير ثابت، أوfn.toString()محقونًا في<script>أو Worker. وهذا هو التحذير الذي يتنبأ بخطأReferenceError.VMDynamicCodeSkipped: تُخطِّي التحويل إلى بايت كود VM لدالة، ولكل دالة معرَّفة بداخلها، لأنها تحتوي على استدعاء مباشر لـevalأو استدعاء ديناميكي لـnew Function/Function. ويصدر أيضًا معeval('literal')الثابت الآمن، لذا فهو يشير إلى فقدان حماية VM لا إلى تعطل أثناء التشغيل. ويتضمن اسم الدالة (إن وُجد) والبنية التي تسببت في التخطي.
لا تعرض حزمة npm javascript-obfuscator التحذيرات في نتيجتها، لذا تقرؤها بوابة CI من استجابة API:
إذ تحمل رسائل result وchunk_end مصفوفة warnings. يستخدم المثال أدناه
readObfuscationResponse()، قارئ البث الوارد في مرجع API، ولا يُفشل البناء إلا عند
DynamicCodeRenameRisk؛ أضف VMDynamicCodeSkipped إلى المرشِّح إذا كان ينبغي أن يمنع فقدانُ حماية VM لدالة ما
الإصدارَ أيضًا.
إذا كان نوع تحذير ما متوقعًا في بنائك وتفضّل كتمه من المصدر بدلًا من تصفيته في CI، فإن
الخيار warnings (v7.8.0+) يتحكم في التحذيرات التي تُصدَر: إذ تكتم القيمة '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(v6.14.0+)منفذ طوارئ كملاذ أخير. عند تفعيله، يحوّل المشوِّش الدالة المحيطة إلى بايت كود رغم ذلك ويكتم التحذير
VMDynamicCodeSkipped. لكنه لا يستطيع إصلاح الاعتماد على النطاق أو على المعرِّفات المُعاد تسميتها: فلا تستخدمه إلا عندما تستطيع ضمان أن الجسم المبني أثناء التشغيل لا يشير أبدًا إلى معرِّف يعيد المشوِّش تسميته، وإلا فإنك تستبدل تشويشًا نظيفًا بخطأReferenceErrorأثناء التشغيل. يظلDynamicCodeRenameRiskيُطلَق كي يستمر CI في الاعتماد عليه لإيقاف البناء. في لوحة التحكم، هذا هو مفتاح "Force Compile Dynamic Code" ضمن مجموعة التجاوزات في قسم VM.
صفحات ذات صلة
- سلوك eval المباشر: نمط فك تغليف IIFE للحد من نطاق ضرر التخطي.
- استهداف الدوال: استخدام
vmTargetFunctionsModeللتفعيل أو الاستبعاد على مستوى الدالة.
