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 exclu, les avertissements émis par l'obfuscateur, et comment diagnostiquer une ReferenceError à l'exécution.
Pourquoi c'est important
L'obfuscation VM compile les corps de fonctions en bytecode exécuté par un interpréteur intégré au runtime. Les identifiants de
la portée environnante sont par ailleurs renommés. Ces deux transformations s'accommodent mal du 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 référence un identifiant que
l'obfuscateur a renommé, vous obtenez Uncaught ReferenceError: <renamed-name> is not defined à la première exécution de la fonction
générée.
L'obfuscateur traite chaque cas différemment. La matrice ci-dessous en donne la version courte ; le reste de cette page détaille chaque cellule.
Ce que fait l'obfuscateur, en un coup d'œil
| Motif dans votre source | Ce qui se passe |
|---|---|
eval('literal string') (le corps est un littéral de chaîne) | Fonctionne correctement. La fonction contenant cet appel perd sa conversion en bytecode VM (un eval direct lit les variables locales environnantes, que la VM ne conserve pas une fois la fonction compilée en bytecode). |
eval(dynamicExpression) | Peut planter à l'exécution avec une ReferenceError. La fonction contenant cet appel — ainsi que toutes les fonctions qui y sont définies — perd elle aussi sa conversion en bytecode VM. |
(0, eval)(s) / window.eval(s) (indirect) | Fonctionne correctement. La fonction contenant cet appel est convertie normalement en bytecode VM. Un eval indirect ne voit pas les variables locales environnantes, le renommage ne peut donc pas le casser. |
new Function('a', 'b', 'return a + b') (tous les arguments sont des littéraux de chaîne) | Fonctionne correctement. La fonction contenant cet appel est convertie normalement en bytecode VM. |
new Function(dynamicBody) / Function(dynamicBody) | Peut planter à l'exécution avec une ReferenceError. La fonction contenant cet appel — ainsi que toutes les fonctions qui y sont définies — perd elle aussi sa conversion en bytecode VM. |
Tous ces motifs sont également remontés sous forme d'avertissements non bloquants dans le résultat d'obfuscation — voir Détecter le problème avant l'exécution ci-dessous pour la forme des avertissements et un extrait à intégrer en CI.
Pourquoi statique et dynamique 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 — or les identifiants ont déjà été
renommés à ce moment-là, si bien que le code construit à l'exécution référence d'anciens noms qui n'existent plus.
new Function(s) fonctionne différemment : le corps s'exécute toujours comme s'il était défini en tête de votre fichier, avec accès
uniquement aux variables globales et jamais aux variables locales entourant l'appel. Cela, en soi, est sans danger — mais 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 les
entrailles), la fonction compilée à l'exécution se heurtera tout de même au même type de ReferenceError.
Un new Function('return 42') statique ne présente jamais ce risque : le corps est une simple chaîne que le renommeur n'inspecte jamais, et
à l'exécution il n'a besoin de voir que les globales. L'obfuscateur laisse l'appel en place et la fonction environnante reste
éligible à la conversion en bytecode VM.
L'erreur observée à l'exécution
Le symptôme habituel est une ReferenceError à la première exécution de la fonction construite dynamiquement :
Ici, TU est un identifiant renommé introduit par l'obfuscateur dans la portée de l'IIFE du bundle. L'appel dynamique à eval ou au
constructeur Function évalue un corps qui le 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, ce qui vous permet de repérer ces motifs en CI avant la mise en production. Deux types d'avertissements sont concernés :
DynamicCodeRenameRisk— une fonction contient un appel dynamique àeval/new Function/Functiondont le corps est construit à l'exécution.VMDynamicCodeSkipped— la conversion en bytecode VM a été ignorée pour une fonction en raison de l'un des motifs ci-dessus. Inclut le nom de la fonction (s'il est disponible) et le type de construction à l'origine de l'exclusion.
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 ce qu'émet getWarnings() : 'none' supprime tout, et une table par type
comme { VMDynamicCodeSkipped: false } ne masque qu'un seul type en conservant les autres.
Contournements
Passer à un eval indirect (
(0, eval)(s))Utile uniquement pour le cas
eval. Un eval indirect s'exécute dans la portée globale et ne voit donc pas les variables locales environnantes — mais, pour cette même raison, il ne peut pas non plus référencer d'identifiants renommés. La fonction contenant l'appel reste convertie en bytecode VM.Rendre le corps entièrement statique
Pour
new Function, si vous pouvez exprimer le corps sous forme d'un seul littéral de chaîne ou littéral de gabarit sans interpolation, l'appel ne présente aucun risque de 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é-encapsuler l'IIFE
Chaque fonction de premier niveau est examinée indépendamment. Isoler l'appel de construction dynamique de code dans sa propre fonction de premier niveau fait que seule cette fonction perd sa conversion en bytecode VM, au lieu de voir l'exclusion se propager à travers une IIFE de premier niveau qui enveloppe tout votre bundle.
Passer à
vmTargetFunctionsMode: 'comment'Mode opt-in : seules les fonctions marquées par
/* javascript-obfuscator:vm */sont converties en bytecode. N'annotez pas la fonction contenant l'appel dynamique et convertissez tout le reste. Consultez Cibler des fonctions.Passer outre l'exclusion avec
vmForceCompileDynamicCode: true(v6.14.0+)Échappatoire de dernier recours. Une fois activée, l'obfuscateur convertit malgré tout la fonction environnante en bytecode et supprime l'avertissement
VMDynamicCodeSkipped. À n'utiliser que si vous pouvez garantir que le corps construit à l'exécution ne référence jamais un identifiant renommé par l'obfuscateur — sinon vous échangez une obfuscation propre contre uneReferenceErrorà l'exécution.DynamicCodeRenameRiskcontinue d'être émis, de sorte que la CI peut toujours s'en servir comme garde-fou. 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
- Obfuscation VM — Comportement de eval direct — le motif de dé-encapsulation d'IIFE pour limiter l'étendue des dégâts de l'exclusion.
- Obfuscation VM — Cibler des fonctions — utiliser
vmTargetFunctionsModepour activer ou désactiver la protection fonction par fonction.
