Ofuscación VM con eval y new Function
Cómo trata la ofuscación VM la construcción dinámica de código (eval directo y el constructor Function), qué se convierte en bytecode y qué se omite, las advertencias que emite el ofuscador y cómo diagnosticar un ReferenceError en tiempo de ejecución.
Por qué importa
La ofuscación VM compila los cuerpos de las funciones a bytecode que despacha un intérprete incluido en el runtime. Los
identificadores del ámbito circundante también se renombran. Ambas transformaciones encajan mal con el código que se
construye a partir de una cadena en tiempo de ejecución: eval(s), new Function(...s) y Function(...s). Si el
código construido en tiempo de ejecución hace referencia a un identificador que el ofuscador ha renombrado, obtienes
Uncaught ReferenceError: <renamed-name> is not defined la primera vez que se ejecuta la función generada.
El ofuscador trata cada patrón de forma distinta. La tabla siguiente es la versión corta; el resto de esta página explica cada fila.
Qué hace el ofuscador, de un vistazo
| Patrón en tu código fuente | Qué ocurre |
|---|---|
eval('literal string') (el cuerpo es un literal de cadena) | Se ejecuta correctamente (salvo que vmForceCompileDynamicCode esté activada, consulta Soluciones alternativas). La función que contiene esta llamada, y todas las funciones definidas dentro de ella, pierden la conversión a bytecode de la VM (el eval directo lee las variables locales circundantes, que la VM no conserva una vez que una función se compila a bytecode). |
eval(dynamicExpression) | Puede fallar en tiempo de ejecución con ReferenceError. La función que contiene esta llamada, y todas las funciones definidas dentro de ella, también pierden la conversión a bytecode de la VM. |
(0, eval)(s) / window.eval(s) (indirecto) | La función que contiene esta llamada se convierte a bytecode de la VM con normalidad. El eval indirecto se ejecuta en el ámbito global y no puede ver las variables locales circundantes, así que las variables locales renombradas no pueden romperlo. Aun así, puede fallar si el código evaluado hace referencia a una variable global que se renombró o eliminó. |
new Function('a', 'b', 'return a + b') (todos los argumentos son literales de cadena) | Se ejecuta correctamente. La función que contiene esta llamada se convierte a bytecode de la VM con normalidad. |
new Function(dynamicBody) / Function(dynamicBody) | Puede fallar en tiempo de ejecución con ReferenceError. La función que contiene esta llamada, y todas las funciones definidas dentro de ella, también pierden la conversión a bytecode de la VM. |
El compilador también detecta eval?.(code) de forma conservadora y lo trata como un eval directo. JavaScript define
esta forma de llamada opcional como eval indirecto; la detección del compilador no cambia ese comportamiento del lenguaje.
Solo algunos de estos patrones generan advertencias no fatales, y solo cuando la llamada está dentro de una función: una llamada
dinámica a eval, new Function o Function notifica tanto DynamicCodeRenameRisk como VMDynamicCodeSkipped
(VMDynamicCodeSkipped solo para una función que la VM compilaría de otro modo: no excluida y, en el modo comment, marcada;
para una función anidada, nombra al ancestro de nivel superior que se omitió), un
eval('...') estático notifica solo VMDynamicCodeSkipped, y el eval indirecto y un new Function totalmente estático no
notifican nada. Una llamada dinámica en el nivel superior de un archivo, fuera de cualquier función, nunca se notifica. Consulta
Detectar el problema antes de la ejecución más abajo para ver los tipos
de advertencia y un fragmento para CI.
La omisión se propaga a través de las IIFE. Si una IIFE de nivel superior envuelve todo tu bundle y cualquier
función de su interior usa un eval directo (estático o dinámico) o una llamada dinámica a new Function / Function, toda la IIFE queda excluida de la conversión a bytecode de
la VM. Consulta El eval() directo desactiva la ofuscación VM para ver el patrón de desenvolver la
IIFE que limita el alcance del daño.
Por qué lo estático y lo dinámico se tratan de forma distinta
eval(s) puede leer y escribir variables locales de la función en la que se llama. Cuando s es un literal de
cadena, el ofuscador puede analizar el cuerpo durante la ofuscación y renombrar los identificadores de forma coherente
con el código circundante. Cuando s es una expresión dinámica, el análisis solo ocurre en tiempo de ejecución, momento
en el que los identificadores ya se han renombrado, así que el código construido en tiempo de ejecución hace referencia
a nombres antiguos que ya no existen.
new Function(s) y el eval indirecto funcionan de otra manera: el cuerpo siempre se ejecuta en el ámbito global, con
acceso solo a las variables globales y nunca a las variables locales que rodean la llamada. No pueden depender de
variables locales renombradas, pero aun así pueden fallar si hacen referencia a variables globales que se renombraron o
eliminaron, o si construyes el cuerpo concatenándole un identificador renombrado (por ejemplo, mediante
func.toString() de una función cuyo interior ha reescrito el ofuscador).
Un cuerpo estático que usa solo sus propios parámetros, como new Function('a', 'b', 'return a + b'), no tiene esa
dependencia: el cuerpo es una cadena simple que el renombrador nunca inspecciona. El ofuscador deja la llamada en su
sitio y la función circundante sigue siendo apta para la conversión a bytecode de la VM. Cambiar solo la sintaxis de
eval no hace que un código dinámico arbitrario sea seguro: revisa las advertencias y prueba el bundle final.
El error que ves en tiempo de ejecución
El síntoma habitual es un ReferenceError la primera vez que se ejecuta la función construida dinámicamente:
Aquí TU es un identificador renombrado que el ofuscador introdujo dentro del ámbito de la IIFE del bundle. La llamada
dinámica a eval o al constructor Function evalúa un cuerpo que hace referencia a él, pero ese cuerpo se ejecuta en un
ámbito donde TU no está definido.
Detectar el problema antes de la ejecución
El ofuscador emite advertencias no fatales a través de la API para que puedas detectar algunos de estos patrones en CI antes
de publicar. Hay dos tipos de advertencia relevantes. Ambos se notifican solo para llamadas dentro de una función, salvo el caso de
.toString(), que se notifica esté donde esté la llamada cuando la función serializada está anidada en otra función o no
se puede resolver (solo en compilaciones VM):
DynamicCodeRenameRisk: una función construye código a partir de una cadena en tiempo de ejecución: unevaldirecto o una llamada anew Function/Functioncuyo cuerpo no es estático, ofn.toString()inyectado en un<script>o un Worker. Es la advertencia que anticipa unReferenceError.VMDynamicCodeSkipped: se omitió la conversión a bytecode de la VM de una función porque contiene unevaldirecto o una llamada dinámica anew Function/Function. En el modo root (el predeterminado), también se omiten todas las funciones definidas dentro de ella; en el modo comment, una función anidada que marques por separado se sigue virtualizando, salvo que contenga ella misma la llamada. También se emite para uneval('literal')estático y seguro, así que indica una pérdida de protección VM más que un fallo en tiempo de ejecución. Incluye el nombre de la función (si está disponible) y la construcción que provocó la omisión.
El paquete npm javascript-obfuscator no expone las advertencias en su resultado, así que una comprobación de CI las lee de la
respuesta de la API: los mensajes result y chunk_end incluyen un array warnings. El ejemplo siguiente usa
readObfuscationResponse(), el lector del stream de la Referencia de la API, y hace fallar la compilación solo con
DynamicCodeRenameRisk; añade VMDynamicCodeSkipped al filtro si perder la protección VM en una función también debe
bloquear la publicación.
Si un tipo de advertencia es esperado en tu compilación y prefieres silenciarlo en el origen en lugar de filtrarlo en CI, la
opción warnings (v7.8.0+) controla qué advertencias se emiten: 'none' lo suprime todo, y un mapa por tipo como
{ VMDynamicCodeSkipped: false } silencia solo un tipo y mantiene el resto.
Soluciones alternativas
Cambia a eval indirecto (
(0, eval)(s))Solo sirve para el caso de
eval. El eval indirecto se ejecuta en el ámbito global, así que no puede ver las variables locales circundantes y, por ese motivo, tampoco puede hacer referencia a variables locales renombradas. La función que contiene la llamada sigue convirtiéndose a bytecode de la VM. Aun así, el código evaluado no debe depender de variables globales que el ofuscador renombre.Haz que el cuerpo sea totalmente estático
Con
new Function, si puedes expresar el cuerpo como un único literal de cadena o de plantilla sin interpolaciones, la llamada no conlleva riesgo de renombrado y la función que la rodea sigue convirtiéndose a bytecode.new Function('a', 'b', 'return a + b')es correcto;new Function('return ' + expr)no lo es.Mueve la llamada a su propia función de nivel superior y desenvuelve la IIFE
Cada función de nivel superior se comprueba de forma independiente. Llevar la llamada de código dinámico a su propia función de nivel superior hace que solo esa función pierda la conversión a bytecode de la VM, en lugar de que la omisión se propague a través de una IIFE de nivel superior que envuelve todo tu bundle.
Cambia a
vmTargetFunctionsMode: 'comment'Modo de inclusión explícita: solo se convierten a bytecode las funciones marcadas con
/* javascript-obfuscator:vm */. No anotes la función que contiene la llamada de código dinámico y marca el resto. Conserva estos comentarios hasta el paso de ofuscación. Consulta Selección de funciones específicas.Anula la omisión con
vmForceCompileDynamicCode: true(v6.14.0+)Vía de escape de último recurso. Cuando está activada, el ofuscador convierte a bytecode la función circundante de todos modos y suprime la advertencia
VMDynamicCodeSkipped. No puede reparar el ámbito: dentro de una función compilada a la fuerza, elevaldirecto no puede leer ni escribir las variables locales ni los parámetros de la función, ni las variables de una función contenedora que también esté virtualizada, aunque el código sea un literal de cadena. Las lecturas lanzanReferenceErrory las escrituras se pierden en silencio. Uneval('...')estático en esta situación no notifica ninguna advertencia, así que una comprobación de CI no lo detectará;DynamicCodeRenameRiskse sigue emitiendo solo para un cuerpo dinámico. Úsala solo cuando el código evaluado no haga referencia a nada más que a variables globales. En el panel, es el interruptor Force Compile Dynamic Code del grupo Anulaciones de la sección VM.
Páginas relacionadas
- El eval() directo desactiva la ofuscación VM: el patrón de desenvolver la IIFE para limitar el alcance de la omisión.
- Selección de funciones específicas: cómo usar
vmTargetFunctionsModepara incluir o excluir funciones una a una.
