Documentación
/

Solución de problemas

/

Ofuscación VM con eval y new Function

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 fuenteQué ocurre
eval('literal string') (el cuerpo es un literal de cadena)Se ejecuta correctamente. 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, 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 eval dinámico o new Function, toda la IIFE queda excluida de la conversión a bytecode de la VM. Consulta Comportamiento de eval directo 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:

Text

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, y ambos se notifican solo para llamadas dentro de una función:

  • DynamicCodeRenameRisk: una función construye código a partir de una cadena en tiempo de ejecución: un eval directo o una llamada a new Function / Function cuyo cuerpo no es estático, o fn.toString() inyectado en un <script> o un Worker. Es la advertencia que anticipa un ReferenceError.
  • VMDynamicCodeSkipped: se omitió la conversión a bytecode de la VM de una función, y de todas las funciones definidas dentro de ella, porque contiene un eval directo o una llamada dinámica a new Function / Function. También se emite para un eval('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 build 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.

Code

Si un tipo de advertencia es esperado en tu build 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.

  • 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 las dependencias de ámbito ni de identificadores renombrados: úsala solo cuando puedas garantizar que el cuerpo construido en tiempo de ejecución nunca hace referencia a un identificador que el ofuscador renombre; de lo contrario, cambias una ofuscación limpia por un ReferenceError en tiempo de ejecución. DynamicCodeRenameRisk se sigue emitiendo para que CI pueda seguir bloqueando con ella. En el panel, es el interruptor "Force Compile Dynamic Code" del grupo Overrides de la sección VM.

Páginas relacionadas