Masquer les noms de fonctions à l'analyse par LLM
Le problème
Vous avez activé vmObfuscation: true, l'avez appliqué à un fichier contenant une fonction comme validateLicense, et
constaté que la sortie obfusquée contient toujours le texte littéral validateLicense - le corps a disparu, remplacé
par du bytecode, mais le nom lui-même reste bien en vue.
Multipliez cela à l'échelle d'une vraie base de code et vous obtenez une liste de noms de fonctions comme
validateLicense, decryptPayload, processPayment, checkSubscription. Un LLM n'a pas besoin de casser le bytecode
pour comprendre ce que fait le programme - les noms suffisent à lui faire produire un résumé assuré et exact du
comportement du module. Le bytecode est opaque ; la table des matières, elle, ne l'est pas.
Pourquoi l'obfuscation VM conserve ces noms
Avec vmTargetFunctionsMode: 'root' (la valeur par défaut), l'obfuscateur transforme le corps de chaque fonction
de premier niveau en bytecode VM, mais laisse délibérément le nom intact. Une déclaration de fonction de premier
niveau est, sémantiquement, une liaison dans la portée environnante - pour un script, l'objet global ; pour un module,
la portée du module (une fonction de premier niveau non exportée a la portée du module et n'est pas globale, mais elle
reste de premier niveau dans le fichier). L'obfuscateur ne peut pas la renommer sans risque, car il n'a aucun moyen de savoir qui
d'autre y fait référence : un autre bundle, un <script> inline, un attribut HTML onclick="validateLicense(...)", une
recherche dynamique window['validateLicense'], etc.
Le compromis fait par défaut est donc le suivant : protéger l'implémentation, préserver la surface publique. Cela évite
de casser les intégrations, mais cela signifie aussi qu'un LLM obtient gratuitement un index de chaque point d'entrée.
Dans ce cas, le résultat de l'obfuscation signale un avertissement VMGlobalFunctionNamesNotRenamed listant les noms
restés lisibles.
Pourquoi c'est important face à la rétro-ingénierie assistée par LLM
Un attaquant humain face à quelques centaines de lignes de dispatch de bytecode abandonnera généralement. Un LLM qui reçoit le même fichier ne prendra même pas la peine d'attaquer le bytecode - il lira les noms, les recoupera avec les quelques littéraux de chaîne visibles, et produira quelque chose comme :
« Ce module protège une fonctionnalité payante. validateLicense vérifie un jeton signé, checkExpiry rejette les
licences expirées, et activateFeature déverrouille l'interface une fois la vérification réussie. L'utilitaire de
déchiffrement se trouve dans decryptPayload. »
Ce résumé suffit à un attaquant pour planifier un contournement ciblé sans jamais toucher à la VM. Ce sont les noms qui constituent la fuite.
La solution : envelopper votre code dans une IIFE
La façon la plus simple et la plus robuste de supprimer cette fuite est de descendre vos fonctions sensibles d'un niveau dans l'arbre des portées. Les fonctions déclarées à l'intérieur d'une autre fonction ne sont pas de premier niveau : l'obfuscateur est donc libre de les renommer et d'intégrer leurs déclarations au bytecode comme n'importe quelle autre instruction.
Une IIFE (Immediately-Invoked Function Expression, expression de fonction immédiatement invoquée) est le moyen le plus léger d'y parvenir - elle ajoute une seule fonction enveloppante qui s'exécute une fois et n'expose rien par son nom.
Avant - noms exposés
Après l'obfuscation VM, validateLicense et checkExpiry subsistent tous deux par leur nom dans la sortie.
Après - noms masqués derrière une IIFE
Les deux déclarations de fonctions se trouvent désormais dans le corps de l'IIFE. L'IIFE elle-même est la seule construction de premier niveau, et une IIFE anonyme n'a aucun nom à divulguer. Après l'obfuscation VM, les noms des fonctions sont renommés et leurs corps convertis en bytecode.
Ce qui a changé. Les fonctions ne sont plus accessibles en tant que globales (c'est ce qui divulguait leurs noms).
Elles restent appelables - mais uniquement depuis l'intérieur de la même IIFE. Si quelque chose avait réellement besoin
d'appeler validateLicense depuis l'extérieur, il ne peut plus l'atteindre ; voir le
modèle du trampoline ci-dessous. Si rien ne le faisait, vous n'avez rien perdu.
Les noms exportés, les identifiants réservés, les chaînes et le comportement observable peuvent rester visibles. Testez vos intégrations après l'enveloppement, en particulier les globales, les exports de modules et le code qui inspecte les noms de fonctions.
Un compromis à connaître : si le corps de l'IIFE contient un eval direct, ou un appel new Function(...) /
Function(...) avec un corps dynamique, l'obfuscation VM ignore toute cette fonction et tout ce qui y est imbriqué (avertissement
VMDynamicCodeSkipped), car le source construit à l'exécution pourrait faire référence à des identifiants renommés par
l'obfuscateur. Le code que vous venez de déplacer dans l'IIFE retombe alors sur l'obfuscation ordinaire et perd sa
protection par bytecode. Utilisez un eval indirect - (0, eval)(...) - ou consultez
l'eval direct avec l'obfuscation VM pour les options possibles.
Et si une fonction doit vraiment être globale ?
Parfois, une fonction est réellement un point d'entrée public - un gestionnaire d'événements inline, un callback JSONP, un hook de SDK tiers. Vous avez deux options :
Exposer un trampoline minimal et garder la logique dans l'IIFE. Déclarez un petit wrapper global dont le seul rôle est d'appeler l'implémentation située dans la portée de l'IIFE. Le nom du trampoline fuit toujours, mais il ne porte aucune information sémantique - nommez-le
__entry1ou quelque chose de semblable - et toute la logique significative reste masquée.Réécrire le site d'appel. Si la globale n'existe que parce qu'un
onclick="validateLicense(...)"inline en a besoin, remplacez le gestionnaire inline par unaddEventListenerdepuis l'intérieur de l'IIFE. Le HTML cesse de nommer la fonction, la fonction n'a plus besoin d'être globale, et la fuite disparaît entièrement.
Initialiseurs de variables de premier niveau : vmWrapTopLevelInitializers
Les déclarations de fonctions ne sont pas les seules à résider à la racine d'un fichier. Les initialiseurs de variables
de premier niveau - constantes de chaîne, objets de configuration, tables de correspondance - sont tout aussi lisibles
dans la sortie lorsqu'ils restent du JavaScript ordinaire. Une ligne comme const API_BASE = '/api/v2/license' en dit
autant à un LLM que function validateLicense.
L'option vmWrapTopLevelInitializers (booléen, false par défaut ; les préréglages VM actuels l'activent) enveloppe
les initialiseurs de premier niveau éligibles dans une IIFE, de sorte que la valeur elle-même est calculée par du
bytecode VM à l'exécution au lieu de figurer dans le source sous forme de littéral. En mode root, les initialiseurs qui
restent en JavaScript ordinaire sont signalés par un avertissement VMTopLevelInitializerNotVirtualized.
Sans l'option
Avec vmWrapTopLevelInitializers: true
Le nom de la liaison (MY_STRING) reste de premier niveau pour la même raison que les noms de fonctions - quelque chose
en dehors du fichier pourrait y faire référence - mais la valeur qu'elle contient est désormais produite par la VM et
n'apparaît plus sous forme de texte lisible.
L'option n'a d'effet que lorsque vmTargetFunctionsMode vaut 'root' (la valeur par défaut) et que vmAsyncExecutor est
désactivé. En mode comment, l'option est sans effet et aucun avertissement n'est signalé. En mode root avec vmAsyncExecutor
(qui ne virtualise que les fonctions asynchrones), elle est également sans effet, et le build signale VMTopLevelInitializerNotVirtualized pour les initialiseurs laissés en JavaScript simple.
Si un fichier ne contient aucune fonction que la VM puisse virtualiser, le résultat signale VMNoFunctionsToVirtualize ;
enveloppez le code à protéger dans une fonction, ou utilisez le mode comment pour sélectionner explicitement
les fonctions sensibles.
Quand cela ne suffit pas
- Noms importés depuis d'autres modules. Si vous regroupez plusieurs fichiers et qu'un module exporte
validateLicensepour qu'un autre l'importe, le bundler gardera ce nom visible dans la sortie groupée, de la même façon que les fonctions de premier niveau restent visibles. Enveloppez le bundle lui-même dans une IIFE (la plupart des bundlers savent le faire), ou déplacez l'export dans une IIFE et réexposez-le via un trampoline sans signification. - Le runtime reste observable. L'obfuscation augmente l'effort nécessaire pour comprendre et modifier le code ; elle ne peut pas garantir que la rétro-ingénierie soit impossible. Gardez les secrets et les décisions de sécurité qui font autorité sur le serveur.
Ne vous tournez pas d'abord vers renameGlobals. Cette option existe et renommera les identifiants de premier
niveau - mais elle n'a aucun moyen de savoir lesquels de ces identifiants sont référencés depuis l'extérieur du fichier
(autres bundles, HTML inline, recherches dynamiques). L'activer casse souvent les intégrations de manière subtile.
L'enveloppement dans une IIFE est plus sûr : il ne renomme rien de global, il cesse simplement de créer des globales
dont vous n'aviez pas besoin.
