Los riesgos de seguridad de un sandbox de JavaScript con el módulo VM de Node.js
22 de febrero de 2023
0 minutos de lectura¿Te encargaron crear un producto que requiere ejecutar JavaScript dinámico proporcionado por usuarios finales? Quizás pienses que construirlo sobre el módulo VM de Node.js es una forma viable de crear un sandbox de JavaScript. En este artículo, veremos por qué está lejos de ser un enfoque recomendable y cuáles son las implicaciones de seguridad.
De vez en cuando, surge un proyecto que pone a prueba el desarrollo backend rutinario y elemental. ¿API? ¿Colas de mensajes? ¿Procesamiento intensivo y requisitos computacionales? No. Aquí tienes una historia pendiente para que la consideres:
Es un caso de uso algo ambiguo y genérico: ejecutar código no confiable de usuarios. Sin embargo, hay ejemplos del mundo real en los que esto sería necesario, como:
El producto replit te permite programar en la nube y ejecutar código en un IDE personalizado
Varias plataformas de práctica de código y entrevistas, como las de “leet code”, te permiten escribir código personalizado, por ejemplo, para implementar un algoritmo determinado o escribir pruebas para uno.
Entra en escena el módulo VM de Node.js.
El módulo VM de Node.js
El uso del módulo principal node:vm permite a los desarrolladores compilar y ejecutar código proporcionado dinámicamente en contextos de la máquina virtual V8. A primera vista, esto podría recordarte a la función de JavaScript eval, que es un riesgo de seguridad conocido. Pero ¿el módulo VM de Node.js es más seguro? Después de todo, dice “máquina virtual”.
Veamos un ejemplo práctico:
En el fragmento de código anterior, la entrada proporcionada por el usuario, que contiene código JavaScript personalizado (indicada por la variable userInputCustomJavaScriptCode), se ejecuta específicamente en un contexto determinado de variables con vm.runInContext(). Este fragmento de código se ejecuta correctamente y arrojaría los siguientes resultados:
Sin embargo, los atacantes podrían aprovechar este tipo de código JavaScript dinámico y manipular otras variables además de las que se asignaron originalmente. En este ejemplo hipotético de Node.js, hay una variable llamada productExpirationDays que establece el tiempo de vencimiento para el usuario. ¿Qué pasaría si un atacante modificara el contenido de su código JavaScript dinámico para establecer un límite más alto?
Si reemplazáramos el ejemplo anterior de entrada del usuario por este —que intenta aumentar los días hasta el vencimiento—, obtendríamos el siguiente error:
El error se debe a que no hay ninguna variable productExpirationDays definida ni existente dentro del alcance del contexto de la máquina virtual en el que se ejecuta el código JavaScript dinámico.
En definitiva, parece que encontramos una forma segura de ejecutar código JavaScript dinámicamente dentro de un sandbox de JavaScript aislado.
Un sandbox de JavaScript inseguro
El ejemplo de código que usa createContext() y vm.runInContext() del módulo VM de Node.js era demasiado simplista. Por desgracia, en realidad, las entradas dañinas de los usuarios suelen recurrir a métodos más astutos, creativos y eficaces para escapar del sandbox de JavaScript.
Veamos cómo un atacante puede proporcionar código inseguro que provoque un ataque de denegación de servicio en una aplicación en ejecución. Considera el siguiente script de Node.js:
En el ejemplo de código anterior, el usuario agregó un bucle infinito, while(true) {}, a su entrada. Aunque el código no modifica otras variables de la aplicación, sí provoca un ataque de denegación de servicio.
Los riesgos de un sandbox de JavaScript inseguro también incluyen la ejecución remota de código. Con this.constructor.constructor, podemos hacer referencia al objeto Function de JavaScript. Una función de JavaScript tiene la característica de aceptar código como una cadena y luego ejecutarlo. Nuestro ataque aprovechará este hecho y, junto con una expresión de función invocada inmediatamente —conocida como IIFE, por sus siglas en inglés—, imprimirá las variables de entorno del proceso de Node.js en ejecución:
Al ejecutar el fragmento de código anterior, se imprimirá el resultado de process.env, que enumera todas las variables de entorno.
A estas alturas, ya deberías darte cuenta del impacto que puede tener la ejecución remota de código cuando se ejecuta código no confiable con el módulo VM de Node.js. Si se permite que los usuarios ejecuten código personalizado, tendrán acceso total al entorno de ejecución del servidor Node.js y podrán iniciar procesos, acceder al sistema de archivos y mucho más.
Resumen
Las consecuencias de tener un sandbox de JavaScript inseguro en un entorno Node.js son graves y podrían detener por completo una aplicación. Vimos cómo un atacante puede aprovecharse del sandbox de JavaScript y proporcionar entradas dañinas que degradan una aplicación Node.js, lo que provoca una vulnerabilidad de denegación de servicio. La superficie de ataque no se limitaba a eso. También vimos que puede producirse una ejecución remota de código, que permite a los atacantes ejecutar código personalizado en entornos de servidor Node.js y poner en riesgo toda la plataforma de la aplicación.
De hecho, la brecha de seguridad que crea un sandbox de JavaScript con el módulo VM de Node.js es apenas el comienzo de una filtración de datos más elaborada que podría ocurrir. Una vez que se compromete un solo servidor de aplicaciones Node.js, puede revelar y exponer información confidencial, como secretos de acceso a una base de datos o a servicios en la nube. Esto permitiría que un atacante ampliara la explotación y se moviera lateralmente dentro de la red implementada.
Por lo tanto, la mejor práctica es no confiar en el módulo VM de Node.js como sandbox seguro para ejecutar código JavaScript no confiable. La documentación de la API de Node.js lo indica claramente: “El módulo node:vm no es un mecanismo de seguridad. No lo uses para ejecutar código no confiable.”
Comienza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.
