5 formas de prevenir la inyección de código en JavaScript y Node.js
6 de abril de 2021
0 minutos de lecturaEscribir código seguro en JavaScript de forma que se evite la inyección de código puede parecer una tarea común, pero hay muchos obstáculos en el camino. Por ejemplo, que tú, como desarrollador, sigas las mejores prácticas de seguridad no significa que los demás hagan lo mismo. Es probable que uses paquetes de código abierto en tu aplicación. ¿Cómo sabes si se desarrollaron de forma segura? ¿Y si contienen código inseguro, como eval()? Veámoslo.
¿Qué es la inyección de código?
La inyección de código es una forma específica de los ataques de inyección, en la que un atacante puede enviar código JavaScript o Node.js que interpreta el navegador o el entorno de ejecución de Node.js. La vulnerabilidad de seguridad se manifiesta cuando el intérprete no logra distinguir entre el código confiable que el desarrollador pretendía ejecutar y el código inyectado que el atacante proporcionó como entrada.
Cómo prevenir la inyección de código
Como convención clave de programación segura, no permitas la ejecución dinámica de código en la aplicación. Esto significa que debes evitar construcciones del lenguaje como eval y las cadenas de código que se pasan a setTimeout() o al constructor Function. En segundo lugar, evita la serialización, que puede ser vulnerable a ataques de inyección que ejecutan código durante el proceso de serialización. Por último, analiza las dependencias para asegurarte de que tu aplicación no sea susceptible a este ataque debido a componentes de código abierto de terceros. Además, si usas una herramienta de análisis estático de código como Snyk Code, puedes encontrar estas posibles vulnerabilidades de seguridad por inyección de código en tu código o en el de tus colegas.
En este artículo veremos 5 formas de prevenir la inyección de código:
Evita
eval(),setTimeout()ysetInterval()Evita
new Function()Evita la serialización de código en JavaScript
Usa un linter de seguridad para Node.js
Usa una herramienta de análisis estático de código (SCA) para encontrar y corregir problemas de inyección de código
1. Evita eval(), setTimeout() y setInterval()
Sé lo que estás pensando: aquí viene otra guía que me dice que evite eval. Sí, es cierto, pero también quiero darte ejemplos reales de otras bibliotecas populares que usaron eval (u otras formas de construcción de código), y que terminaron pagando las consecuencias con una vulnerabilidad de seguridad grave.
Pero antes de profundizar en las referencias a paquetes vulnerables de terceros, expliquemos primero eval y sus funciones relacionadas. Los entornos de ejecución de JavaScript, como el navegador y la plataforma Node.js del lado del servidor, permiten evaluar y ejecutar código durante el tiempo de ejecución. Un ejemplo práctico es el siguiente:
Con esto, un programador intenta crear una forma dinámica de acceder a los datos del DOM. En este ejemplo, se supone que getElementform también puede estar bajo el control del usuario, al igual que la variable elementId. Hay mejores formas de realizar esta tarea sin necesidad de usar eval, así que, en la medida de lo posible, evita el código dinámico como este.
En Node.js, alguien podría querer permitir el acceso a puntos de datos específicos de la aplicación mediante una evaluación dinámica. Este es un ejemplo:
En este ejemplo, se supone que el archivo exacto que queremos requerir es dinámico y puede estar bajo el control del usuario; por lo tanto, también existe la posibilidad de vulnerabilidades de seguridad por inyección de código.
La inyección de código en Dustjs muestra un ejemplo real del uso inseguro de eval
El paquete npm de LinkedIn dustjs, un proyecto de plantillas asíncronas para el navegador y Node.js del lado del servidor, muestra lo grave que puede llegar a ser una vulnerabilidad de inyección de código.
Aunque este paquete ya no recibe mucho mantenimiento, sigue registrando cerca de 72,000 descargas mensuales y tuvo que enfrentar una vulnerabilidad de seguridad por inyección de código.
Los responsables de mantenimiento de dustjs hicieron todo lo posible para escapar las entradas de usuario potencialmente peligrosas que podían llegar a construcciones de código inseguras como la función eval(). Sin embargo, la función escapeHtml tenía una falla de seguridad: solo verificaba si el tipo era una cadena y luego escapaba la entrada, cuando también debería haber verificado otros tipos, como los arreglos. Este pull request corrigió la vulnerabilidad de seguridad por inyección de código:

¿Y qué tiene que ver eval() con todo esto?
Si usas dustjs, también podrías agregar el paquete npm dustjs-helpers para obtener ayudantes de plantillas adicionales, como operaciones matemáticas y lógicas. Uno de esos ayudantes adicionales es una condición if, que podrías usar así en tus propios archivos de plantillas dust:

Tiene sentido, ¿verdad?
El problema es que la entrada de usuario no controlada en ese parámetro de consulta device llega directamente al ayudante de condición if, que usa eval, como puedes ver en la línea 227, para evaluar la condición de forma dinámica:

Ahora todo queda claro y se ve cómo varios problemas de seguridad se combinan de una forma imprevista:
dustjs-linkedin, un paquete de código abierto, tiene una falla de seguridad que hace que las cadenas de entrada no se saniticen correctamente en su función
escapeHtml.dustjs-helpers, un paquete de código abierto, usa una convención de programación insegura, como la función
eval(), para evaluar código de forma dinámica durante el tiempo de ejecución.
¿Quieres ver cómo aproveché esta vulnerabilidad y vulneré una aplicación real en funcionamiento, usando exactamente esta vulnerabilidad? Échale un vistazo:

Evita también setTimeout() y setInterval()
Para cerrar el tema de la práctica recomendada de evitar eval(), también quiero mencionar otras funciones de las que, como desarrollador de JavaScript, seguramente has oído hablar o que has usado al menos una vez en tu aplicación: setTimeout() y setInterval().
Un dato que se conoce menos sobre estas funciones es que también reciben cadenas de código. Por ejemplo, se pueden usar así:
¡Por suerte, los literales de cadena no están permitidos en un entorno Node.js!
2. Evita new Function()
Otra construcción del lenguaje, similar a eval(), setTimeout() y setInterval(), es el constructor Function, que permite definir una función de forma dinámica a partir de literales de cadena.
Considera el siguiente ejemplo sencillo:
Si has seguido la explicación hasta aquí, ya sabes qué posibles problemas de seguridad podrían surgir si la entrada del usuario llega a una función como esta...
3. Evita la serialización de código en JavaScript
La serialización es bastante común en el ecosistema Java. Mi amigo Brian Vermeer escribió una publicación de blog sobre cómo las vulnerabilidades de seguridad afectan a las aplicaciones Java debido a operaciones de serialización inseguras. Te recomiendo leerla: Serialización y deserialización en Java: explicación de la vulnerabilidad de deserialización de Java.
Volvamos al mundo de JavaScript: al parecer, la serialización también es común aquí.
Es probable que no escribas tu propia lógica de serialización y deserialización, pero en el maravilloso mundo de npm, con más de 1,500,000 paquetes de código abierto a tu disposición, ¿por qué no usarlos?
js-yaml es muy popular, con más de 28,000,000 de descargas por semana, y según Snyk Advisor, tiene un buen estado general:

Dicho esto, en la captura de pantalla anterior del paquete npm js-yaml puedes ver que las versiones anteriores tenían vulnerabilidades de seguridad. ¿Cuál, te preguntarás?
Se descubrió que algunas versiones de js-yaml eran vulnerables a la ejecución de código debido a la deserialización. La vulnerabilidad se manifiesta por el siguiente uso del constructor new Function():
Veamos cómo sería una prueba de concepto de explotación de esta vulnerabilidad:
Por lo tanto, si un actor malicioso puede proporcionar una entrada como esta, o parte de ella, que se usa para crear la variable x en el código de prueba de concepto anterior, una posible vulnerabilidad se convierte en un peligro real.
La vulnerabilidad anterior se remonta a 2013, pero un informe de vulnerabilidad de seguridad de 2019 encontró otro caso de ejecución de código arbitrario en js-yaml. Así que ten cuidado o, para darte un consejo más práctico y útil, evita new Function() y analiza tus paquetes de código abierto de terceros para asegurarte de no tener estas vulnerabilidades y de poder corregirlas automáticamente con un pull request de corrección si las tienes.
4. Usa un linter de seguridad para Node.js
Llegamos a la parte de herramientas de esta guía: hablemos de los linters. A los desarrolladores de JavaScript les gustan los linters. Ya sea que uses standardjs o eslint para aplicar un estilo de código, estas herramientas son muy comunes en cualquier proyecto de JavaScript o Node.js.
¿Por qué no aplicar también buenas prácticas de seguridad? Aquí es donde entra en juego eslint-plugin-security. Según las instrucciones del README, usar el complemento es bastante sencillo. Solo agrega la siguiente configuración del complemento eslint para habilitar la configuración recomendada:
¿Cómo ayuda el linter?
Tiene reglas para detectar convenciones de programación inseguras, como detect-eval-with-expression, que detecta el uso de eval() con expresiones o literales de cadena. El linter también tiene otras reglas, como la que detecta el uso de las API child_process de Node.js.
Ten en cuenta que la última publicación de eslint-plugin-security fue hace más de 4 años. Aunque todavía podría funcionar bien, tal vez quieras considerar paquetes sucesores como eslint-plugin-security-node.
5. Usa una herramienta de análisis estático de código para encontrar y corregir problemas de inyección de código
Los linters de análisis estático de código (SCA) en sus formas básicas, como los que se usan con ESLint, son un buen punto de partida. Proporcionan suficiente contexto para aplicar un estilo de código, pero, como vimos con el linter de seguridad para Node.js, no son lo bastante flexibles como para abordar realmente los problemas de seguridad.
Algunas de las preocupaciones de los desarrolladores respecto a un linter de seguridad para Node.js, como eslint-plugin-security, son:
Falsos positivos: Las reglas del linter son bastante básicas y podrían generar muchos falsos positivos, lo que solo aumenta la frustración y la confusión de los desarrolladores. Por ejemplo, la siguiente expresión
RegExp(matchEmailRegEx)hará que el linter de seguridad de Node.js genere un error debido al uso de la función RegExp con un valor que no es literal. ¿Y simatchEmailRegExfuera solo una constante en mi archivo shared/variables.js? El linter no es lo bastante avanzado como para saberlo.Las reglas son demasiado rígidas: Como continuación del punto anterior, la regla es demasiado rígida. O usas
child_process.exec(someCommand, [])o no lo haces. El proceso de análisis estático de código que usa el linter no es lo bastante inteligente como para saber quesomeCommandes una constante que definiste directamente en el código. El solo hecho de usar child_process.exec() con un valor no literal basta para generar un error del linter, lo que termina frustrando a los desarrolladores y hace que desactiven la regla.Las reglas son demasiado básicas: El conjunto de reglas es demasiado pequeño y los hallazgos son demasiado básicos. En la práctica, es todo o nada, sin mucho contexto sobre cómo fluyen realmente los datos desde una entrada determinada del usuario hasta código potencialmente sensible, como la ejecución de comandos, las consultas SQL u otros.
Para retomar lo que dije antes, un linter de seguridad como eslint-plugin-security-node u otros es un buen punto de partida. Sin duda, es mejor que no tener nada.
Pero hay mejores formas de encontrar problemas de seguridad en tu propio código mientras programas.
Te presento Snyk Code, una herramienta de pruebas de seguridad de aplicaciones estáticas (SAST) diseñada para desarrolladores.
Cómo encontrar una inyección de comandos en una aplicación Node.js
Snyk Code se lanzará pronto, pero te mostraré un adelanto de cómo funciona.
Primero, conéctate a Snyk con una cuenta de GitHub y luego importa el repositorio de GitHub. Para hacerlo, haz clic en Agregar proyecto y luego en el ícono de GitHub:

Luego, busca un repositorio en la lista o escríbelo en el cuadro de búsqueda y activa el interruptor del repositorio para comenzar el análisis:

Snyk importará el repositorio de GitHub y lo analizará rápidamente.
También detectará automáticamente otros archivos de manifiesto relacionados con posibles problemas de seguridad; por ejemplo, si usas dependencias de código abierto con vulnerabilidades conocidas o si tu imagen de Docker introduce varias vulnerabilidades de seguridad.
Enfoquémonos en nuestro propio código en esta aplicación de Node.js. Para ver qué encontramos, hagamos clic en Análisis de código:

Snyk Code encontró varias vulnerabilidades. Una de ellas es una vulnerabilidad de inyección de comandos, como vemos aquí:

Los problemas de seguridad detectados en esta línea de código explican el motivo de preocupación:
Pero ¿cómo fluyen los datos desde el parámetro url hasta la función insegura exec()? Haz clic en el botón detalles completos para ver una versión más detallada del flujo de datos y obtener más contexto:

Aquí podemos ver claramente el panorama completo tal como lo analizó Snyk Code.
El parámetro url se crea a partir del arreglo item, que a su vez es la fuente de una entrada controlada por el usuario, que fluye como el cuerpo de un mensaje en la variable req.body.content.
Cómo corregir la inyección de comandos
Ahora podemos tomar medidas adicionales para resolver el problema de seguridad, como:
En lugar de usar la función insegura
exec(), podemos usar la versión segura de esa API:execFile(), que se encarga de escapar los argumentos proporcionados como elementos de un arreglo.Podemos y debemos validar, escapar o sanear la variable item proveniente de la entrada del usuario antes de que llegue a código sensible, como el que ejecuta procesos del sistema.
Resumen
¡Excelente trabajo si llegaste hasta aquí!
Esperamos que ahora tengas más conciencia de los problemas que pueden causar las vulnerabilidades de inyección de código, ya sea que se originen en tu propio código o en dependencias de terceros que importas en tu aplicación.
Si esta publicación te resultó útil, aquí tienes algunas lecturas recomendadas de mis colegas de Snyk:
Si tú o tu equipo desarrollan en Go, consulta esta guía de seguridad de Go: 8 prácticas recomendadas de seguridad para desarrolladores de Go
Si trabajas con Java y, en particular, con Spring MVC, te interesará esta lectura: Cómo resolver problemas de seguridad de Java en mi aplicación de Spring MVC, donde también se detectan problemas de seguridad con Snyk Code
Comienza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.
