تشويش 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') (الجسم نص حرفي) | يعمل بشكل صحيح (ما لم يكن vmForceCompileDynamicCode مفعَّلًا، انظر حلول بديلة). لكن الدالة التي تحتوي هذا الاستدعاء، وكل دالة معرَّفة بداخلها، تفقد التحويل إلى بايت كود 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 كليهما
(VMDynamicCodeSkipped فقط لدالة كانت الآلة الافتراضية ستترجمها لولا ذلك: غير مستبعَدة، ومعلَّمة في وضع التعليقات؛
وفي حالة الدالة المتداخلة يذكر التحذير الدالة الأصل في المستوى الأعلى التي جرى تخطيها)، واستدعاء eval('...') الثابت
لا يُبلغ إلا عن VMDynamicCodeSkipped، أما eval غير المباشر وnew Function الثابت بالكامل فلا يُبلغان عن شيء. والاستدعاء
الديناميكي على المستوى الأعلى من الملف، خارج أي دالة، لا يُبلَغ عنه أبدًا. انظر
اكتشاف المشكلة قبل وقت التشغيل أدناه لأنواع التحذيرات ومقتطف كود لبيئة CI.
يمتد التخطي عبر دوال IIFE. إذا كانت دالة IIFE في المستوى الأعلى تغلّف حزمتك بالكامل وكانت أي دالة بداخلها تستخدم
استدعاءً مباشرًا للدالة eval (ثابتًا أو ديناميكيًا) أو استدعاءً ديناميكيًا للمُنشئ new Function / Function، فستُستبعد دالة IIFE بأكملها من التحويل إلى بايت كود VM. انظر
استدعاء eval() المباشر يعطّل تشويش VM لنمط فك تغليف 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 قبل النشر.
ويعنينا هنا نوعان من التحذيرات. ولا يُبلَغ عن أيٍّ منهما إلا للاستدعاءات الواقعة داخل دالة، باستثناء حالة .toString()،
التي يُبلَغ عنها أينما وقع الاستدعاء عندما تكون الدالة المُسلسَلة متداخلة في دالة أخرى أو يتعذّر
تحديدها (في بناءات VM فقط):
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. لكنه لا يستطيع إصلاح النطاق: داخل دالة مُترجَمة قسرًا، لا يستطيعevalالمباشر قراءة المتغيرات المحلية للدالة أو معاملاتها أو الكتابة فيها، ولا متغيرات دالة محيطة محوَّلة هي الأخرى إلى الآلة الافتراضية، حتى عندما يكون الكود نصًا حرفيًا. تُطلق القراءةُ خطأReferenceError، وتضيع الكتابة بصمت. واستدعاءeval('...')الثابت في هذه الحالة لا يُبلغ عن أي تحذير على الإطلاق، فلن يلتقطه فحص CI؛ ولا يُطلَقDynamicCodeRenameRiskإلا للجسم الديناميكي. لا تستخدمه إلا عندما لا يشير الكود المُقيَّم إلى شيء سوى المتغيرات العامة. في لوحة التحكم، هذا هو مفتاح Force Compile Dynamic Code ضمن مجموعة التجاوزات في قسم VM.
صفحات ذات صلة
- استدعاء eval() المباشر يعطّل تشويش VM: نمط فك تغليف IIFE للحد من نطاق ضرر التخطي.
- استهداف دوال بعينها: استخدام
vmTargetFunctionsModeللتفعيل أو الاستبعاد على مستوى الدالة.
