Documentation
/

Dépannage

/

Obfuscation VM avec eval et new Function

Obfuscation VM avec eval et new Function

Comment l'obfuscation VM gère la construction dynamique de code (eval direct et constructeur Function), ce qui est converti en bytecode ou ignoré, les avertissements émis par l'obfuscateur, et comment diagnostiquer une ReferenceError à l'exécution.

Pourquoi c'est important

L'obfuscation VM compile le corps des fonctions en bytecode, exécuté par un interpréteur intégré au runtime. Les identifiants de la portée environnante sont eux aussi renommés. Ces deux transformations s'accordent mal avec le code construit à partir d'une chaîne à l'exécution : eval(s), new Function(...s) et Function(...s). Si le code construit à l'exécution fait référence à un identifiant que l'obfuscateur a renommé, vous obtenez Uncaught ReferenceError: <renamed-name> is not defined dès la première exécution de la fonction générée.

L'obfuscateur traite chaque motif différemment. Le tableau ci-dessous en donne la version courte ; le reste de la page détaille chaque ligne.

Ce que fait l'obfuscateur, en un coup d'œil

Motif dans votre sourceCe qui se passe
eval('literal string') (le corps est un littéral de chaîne)S'exécute correctement. La fonction qui contient cet appel - ainsi que toutes les fonctions définies à l'intérieur - perd la conversion en bytecode VM (un eval direct lit les variables locales environnantes, que la VM ne préserve pas une fois la fonction compilée en bytecode).
eval(dynamicExpression)Peut planter à l'exécution avec une ReferenceError. La fonction qui contient cet appel - ainsi que toutes les fonctions définies à l'intérieur - perd aussi la conversion en bytecode VM.
(0, eval)(s) / window.eval(s) (indirect)La fonction qui contient cet appel est convertie en bytecode VM normalement. Un eval indirect s'exécute dans la portée globale et ne voit pas les variables locales environnantes : des variables locales renommées ne peuvent donc pas le casser. Il peut néanmoins échouer si le code évalué fait référence à une globale renommée ou supprimée.
new Function('a', 'b', 'return a + b') (tous les arguments sont des littéraux de chaîne)S'exécute correctement. La fonction qui contient cet appel est convertie en bytecode VM normalement.
new Function(dynamicBody) / Function(dynamicBody)Peut planter à l'exécution avec une ReferenceError. La fonction qui contient cet appel - ainsi que toutes les fonctions définies à l'intérieur - perd aussi la conversion en bytecode VM.

Le compilateur détecte aussi eval?.(code) de manière prudente et le traite comme un eval direct. JavaScript définit cette forme d'appel optionnel comme un eval indirect ; la détection du compilateur ne modifie pas ce comportement du langage.

Seuls certains de ces motifs produisent des avertissements non bloquants, et uniquement lorsque l'appel se trouve dans une fonction : un appel dynamique à eval, new Function ou Function signale à la fois DynamicCodeRenameRisk et VMDynamicCodeSkipped, un eval('...') statique ne signale que VMDynamicCodeSkipped, et un eval indirect ou un new Function entièrement statique ne signalent rien. Un appel dynamique au niveau racine d'un fichier, hors de toute fonction, n'est jamais signalé. Voir Détecter le problème avant l'exécution ci-dessous pour les types d'avertissements et un extrait pour la CI.

L'exclusion se propage à travers les IIFE. Si une IIFE de premier niveau enveloppe tout votre bundle et qu'une fonction à l'intérieur utilise un eval ou un new Function dynamique, toute l'IIFE est exclue de la conversion en bytecode VM. Voir Comportement de eval direct pour le motif de désenveloppement de l'IIFE qui limite l'étendue des dégâts.

Pourquoi les cas statiques et dynamiques sont traités différemment

eval(s) peut lire et écrire les variables locales de la fonction dans laquelle il est appelé. Lorsque s est un littéral de chaîne, l'obfuscateur peut analyser le corps au moment de l'obfuscation et renommer les identifiants de façon cohérente avec le code environnant. Lorsque s est une expression dynamique, l'analyse n'a lieu qu'à l'exécution

  • et à ce moment-là, les identifiants ont déjà été renommés : le source construit à l'exécution fait donc référence à d'anciens noms qui n'existent plus.

new Function(s) et l'eval indirect fonctionnent différemment : le corps s'exécute toujours dans la portée globale, avec accès uniquement aux variables globales et jamais aux variables locales autour de l'appel. Ils ne peuvent pas dépendre de variables locales renommées, mais peuvent quand même échouer s'ils font référence à des globales renommées ou supprimées

  • ou si vous construisez le corps en y concaténant un identifiant renommé (par exemple via func.toString() d'une fonction dont l'obfuscateur a réécrit l'intérieur).

Un corps statique qui n'utilise que ses propres paramètres, comme new Function('a', 'b', 'return a + b'), n'a pas ce type de dépendance : le corps est une simple chaîne que le renommage n'inspecte jamais. L'obfuscateur laisse l'appel en place et la fonction environnante reste éligible à la conversion en bytecode VM. Changer la seule syntaxe d'eval ne rend pas sûr un code dynamique arbitraire - examinez les avertissements et testez le bundle final.

L'erreur que vous voyez à l'exécution

Le symptôme courant est une ReferenceError dès la première exécution de la fonction construite dynamiquement :

Text

Ici, TU est un identifiant renommé que l'obfuscateur a introduit dans la portée de l'IIFE du bundle. L'appel dynamique à eval / au constructeur Function évalue un corps qui y fait référence, mais ce corps s'exécute dans une portée où TU n'est pas défini.

Détecter le problème avant l'exécution

L'obfuscateur émet des avertissements non bloquants via l'API afin que vous puissiez repérer certains de ces motifs en CI avant la mise en production. Deux types d'avertissements sont concernés, et tous deux ne sont signalés que pour des appels situés dans une fonction :

  • DynamicCodeRenameRisk - une fonction construit du code à partir d'une chaîne à l'exécution : un eval direct ou un appel new Function / Function dont le corps n'est pas statique, ou fn.toString() injecté dans un <script> ou un Worker. C'est l'avertissement qui annonce une ReferenceError.
  • VMDynamicCodeSkipped - la conversion en bytecode VM a été ignorée pour une fonction, et pour toutes les fonctions définies à l'intérieur, parce qu'elle contient un eval direct ou un appel dynamique à new Function / Function. Il se déclenche aussi pour un eval('literal') statique sans danger : il signale donc une perte de protection VM plutôt qu'un plantage à l'exécution. Inclut le nom de la fonction (s'il est disponible) et la construction qui a déclenché l'exclusion.

Le paquet npm javascript-obfuscator n'expose pas les avertissements dans son résultat : une vérification en CI les lit donc dans la réponse de l'API, où les messages result et chunk_end portent un tableau warnings. L'exemple ci-dessous utilise readObfuscationResponse(), le lecteur de flux de la Référence de l'API, et ne fait échouer le build que sur DynamicCodeRenameRisk ; ajoutez VMDynamicCodeSkipped au filtre si la perte de la protection VM sur une fonction doit aussi bloquer la publication.

Code

Si un type d'avertissement est attendu dans votre build et que vous préférez le faire taire à la source plutôt que de le filtrer en CI, l'option warnings (v7.8.0+) contrôle les avertissements émis : 'none' supprime tout, et une table par type comme { VMDynamicCodeSkipped: false } ne fait taire qu'un seul type en conservant les autres.

Solutions de contournement

  • Passer à un eval indirect ((0, eval)(s))

    Utile uniquement dans le cas d'eval. Un eval indirect s'exécute dans la portée globale : il ne voit donc pas les variables locales environnantes - et pour cette raison, il ne peut pas non plus faire référence à des variables locales renommées. La fonction qui contient l'appel reste convertie en bytecode VM. Le code évalué ne doit toutefois pas dépendre de globales que l'obfuscateur renomme.

  • Rendre le corps entièrement statique

    Pour new Function, si vous pouvez exprimer le corps sous la forme d'un unique littéral de chaîne ou d'un gabarit littéral sans interpolation, l'appel ne présente aucun risque lié au renommage et la fonction qui l'entoure reste convertie en bytecode. new Function('a', 'b', 'return a + b') convient ; new Function('return ' + expr) non.

  • Déplacer l'appel dans sa propre fonction de premier niveau, et désenvelopper l'IIFE

    Chaque fonction de premier niveau est vérifiée indépendamment. Extraire l'appel de code dynamique dans sa propre fonction de premier niveau fait que seule cette fonction perd la conversion en bytecode VM, au lieu que l'exclusion se propage à travers une IIFE de premier niveau qui enveloppe tout votre bundle.

  • Passer à vmTargetFunctionsMode: 'comment'

    Mode à activation explicite : seules les fonctions marquées avec /* javascript-obfuscator:vm */ sont converties en bytecode. N'annotez pas la fonction qui contient l'appel de code dynamique, et marquez les autres. Conservez ces commentaires jusqu'à l'étape d'obfuscation. Voir Cibler des fonctions.

  • Forcer la compilation malgré l'exclusion avec vmForceCompileDynamicCode: true (v6.14.0+)

    Échappatoire de dernier recours. Lorsqu'elle est activée, l'obfuscateur convertit malgré tout la fonction environnante en bytecode et supprime l'avertissement VMDynamicCodeSkipped. Elle ne peut pas réparer les dépendances de portée ou d'identifiants renommés : ne l'utilisez que si vous pouvez garantir que le corps construit à l'exécution ne fait jamais référence à un identifiant que l'obfuscateur renomme - sinon, vous échangez une obfuscation propre contre une ReferenceError à l'exécution. DynamicCodeRenameRisk continue de se déclencher, de sorte que la CI peut toujours s'appuyer dessus. Dans le tableau de bord, il s'agit de l'interrupteur « Force Compile Dynamic Code » dans le groupe Surcharges de la section VM.

Pages associées