Vulnerabilidades en extensiones de complementos C/C++ de NodeJS
Alessio Della Libera
14 de agosto de 2024
0 minutos de lecturaUno de los objetivos principales de esta investigación fue explorar las vulnerabilidades de C/C++ en el contexto de los paquetes npm de NodeJS. Nos centraremos en explorar e identificar vulnerabilidades clásicas, como desbordamientos de búfer, denegación de servicio (bloqueo del proceso, tipos sin validar) y fugas de memoria en complementos C/C++ de NodeJS, y en modelar fuentes, destinos y sanitizadores pertinentes con Snyk Code (consulta Snyk lleva un enfoque de AppSec centrado en los desarrolladores a C/C++).
Los objetivos de esta investigación son paquetes de NPM que usan interfaces C/C++ como parte de su implementación. No analizamos proyectos que no aparezcan en NPM.
En esta publicación, presentamos un resumen de las vulnerabilidades de seguridad comunes y los patrones vulnerables que pueden surgir al escribir complementos C/C++ en NodeJS. También compartiremos ejemplos de correcciones y sugerencias para quienes mantienen proyectos de código abierto.
Esta publicación se inspiró en el artículo “Bilingual Problems: Studying the Security Risks Incurred by Native Extensions in Scripting Languages”, de Cristian-Alexandru Staicu, Sazzadur Rahaman, Àgnes Kiss y Michael Backes.[1] En el artículo original, los autores analizaron los riesgos de seguridad de las extensiones nativas en lenguajes populares, incluido JavaScript.
Antecedentes de los complementos C/C++ de NodeJS
NodeJS ofrece distintas API para llamar código nativo C/C++. El objetivo de esta investigación es analizar las vulnerabilidades de seguridad que pueden surgir al usar alguno de los siguientes mecanismos:
node_api.h: Node-API
En GitHub encontrarás un buen recurso con ejemplos para usar las bibliotecas anteriores.
Para obtener una introducción completa a los complementos y saber cómo compilarlos, consulta la documentación oficial de NodeJS.
Las vulnerabilidades analizadas y detectadas en al menos un paquete son:
Fugas de memoria
Tipo sin validar (DoS)
Aserción alcanzable (DoS)
Excepciones no controladas (DoS)
Desbordamiento de búfer
Desbordamiento de enteros
En las siguientes secciones, se presentan ejemplos de patrones vulnerables y se explican las condiciones que deben cumplirse para que la vulnerabilidad pueda explotarse.
Ejemplos de patrones vulnerables
En esta sección, veremos cómo las API específicas de los complementos pueden provocar problemas de seguridad si no se manejan correctamente, además de algunos patrones vulnerables detectados durante este estudio.
NOTA: Los siguientes ejemplos no constituyen una lista exhaustiva. Puede haber más situaciones
que provoquen problemas de seguridad y que no se aborden en esta publicación.
Configuración
Instala node-gyp (https://github.com/nodejs/node-gyp).
Los siguientes archivos se usan para ejecutar los ejemplos de la próxima sección:
package.json
binding.gyp
Ejecuta los siguientes comandos para compilar las extensiones C/C++:
node-gyp configurenode-gyp build
Ejecutar un ejemplo específico:
main.js
Excepciones no controladas
Impacto: denegación de servicio (DoS)
napi
La API napi ofrece distintas funciones para controlar excepciones y lanzar errores. Sin embargo, según la marca que se use en el archivo binding.gyp, hay que tener cuidado para evitar cierres inesperados.
Por ejemplo, si la marca NAPI_DISABLE_CPP_EXCEPTIONS está establecida en el archivo binding.gyp, las siguientes situaciones pueden provocar el cierre del proceso (DoS):
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();, además de otras funciones que pueden generar un error (por ejemplo, un argumento de tipo incorrecto)throw Napi::Error::Newsin estar dentro de un bloquetry/catchVarias llamadas a
Napi::TypeError::New(env, "").ThrowAsJavaScriptException();sin unreturnque puedan alcanzarse dentro de la misma función
Como se explica en la documentación, “después de lanzar una excepción de JavaScript, el código generalmente debe regresar de inmediato desde la función de retorno de llamada nativa, después de realizar cualquier limpieza necesaria”. .
test_napi_exceptions.cpp
Ejecuta estos ejemplos:
Aserción alcanzable
Impacto: denegación de servicio (DoS)
node_api
Al revisar los ejemplos proporcionados, vemos que en algunos ejemplos se usa assert para comprobar el valor de retorno de algunas funciones. Sin embargo, si durante la ejecución del programa se alcanza un assert con valores contaminados (provenientes del código JavaScript), puede producirse un cierre (DoS). Al revisar algunos proyectos, encontramos varias aserciones alcanzables en la lógica del código, así que me pareció importante mencionarlas en la lista anterior.
Una posible solución para esta situación sería comprobar el valor de retorno con un if y luego devolver el valor adecuado (según la lógica del programa), en lugar de usar un assert.
test_node_api_assert.c
Ejecuta este ejemplo:
Tipo de datos sin validar
Impacto: denegación de servicio (DoS)
napi
napi ofrece varias API para convertir tipos de JavaScript. Por ejemplo,
Napi::Value::ToString() “devuelve el valor Napi::Value convertido en una cadena de JavaScript”. De forma similar, Napi::Value::ToNumber() “devuelve el valor Napi::Value convertido en un número de JavaScript”.
Internamente, la API Napi::Value::ToString() de napi llama a napi_coerce_to_string de Node-API:
De forma similar, internamente la API Napi::Value::ToNumber() de napi llama a napi_coerce_to_number de Node-API:
Según la documentación oficial de napi_coerce_to_string: “Esta API implementa la operación abstracta ToString(), tal como se define en la sección 7.1.13 de la especificación del lenguaje ECMAScript. Esta función puede ejecutar código JS si el valor recibido es un objeto”. Esto significa que, si la entrada del usuario define una propiedad toString, se devolverá el valor de esa propiedad (en lugar de llamar a toString()), lo que puede generar resultados inesperados.
Si llamamos a otros métodos sobre los valores que devuelve Napi::Value::ToString() y la entrada define una propiedad toString, puede producirse una excepción que, en la mayoría de los casos, provoca el cierre del proceso. Lo mismo ocurre con napi_coerce_to_number.
Patrón vulnerable:
llamadas como
Napi::String::Utf8Value()sobre unNapi::Valueobtenido deToString()oToNumbersin comprobar correctamente el tipo
Una posible solución para evitar estas situaciones es comprobar que el valor devuelto por Napi::Value::ToString() o Napi::Value::ToNumber() sea, respectivamente, una cadena o un número antes de llamar a otros métodos sobre esos valores.
NOTA: Al igual que en los casos de excepciones no controladas mencionados antes, estos problemas ocurren si la marca NAPI_DISABLE_CPP_EXCEPTIONS está establecida en el archivo binding.gyp.
test_napi_unchecked_type.cpp
Ejecuta estos ejemplos:
Fugas de memoria
Impacto: divulgación de información
napi
La API napi ofrece varios métodos para crear un valor de cadena de JavaScript a partir de una cadena C codificada en UTF-8, UTF-16-LE o ISO-8859-1. Estas API son:
Todos estos métodos tienen la misma firma:
El valor que se debe revisar cuidadosamente es [in] length, es decir, la longitud de la cadena en bytes. Si un atacante controla este valor o está codificado de forma fija y el valor de entrada está contaminado, es posible almacenar valores de memoria inesperados en result.
Para evitar estos problemas, usa NAPI_AUTO_LENGTH como valor de longitud de tipo size_t length.
Patrón vulnerable:
napi_create_string_*consize_t lengthsuperior a la longitud deconst char* str
test_napi_memory_leak.c
Ejecuta este ejemplo:
Metodología
Para probar y encontrar automáticamente la mayor cantidad posible de problemas, usé el siguiente enfoque para aprovechar las capacidades de Snyk Code:
Crear un conjunto de datos de paquetes npm que llaman a C/C++ mediante las API de complementos de NodeJS
Escribir reglas de seguridad en Snyk Code para modelar:
Fuentes: en este contexto, las fuentes son valores provenientes del código JavaScript, que pueden ser datos de
Napi::CallbackInfo::Env()en el contexto denapio denapi_get_value_*en el contexto denode_apiDestinos: según el problema de seguridad, modelé la presencia de varias llamadas a
ThrowAsJavaScriptExceptiondentro de una misma función, la comprobaciónasserty varios métodos usados para crear valores de cadena (por nombrar algunos). También tuve en cuenta los casos en que el código no es vulnerable debido a la presencia de ciertos argumentos, comoNAPI_AUTO_LENGTHpara los problemas de fugas de memoria
Escribir reglas que usen los destinos y las fuentes definidos para realizar un análisis de contaminación y rastrearla desde las fuentes hasta los destinos
Usar las fuentes definidas en las reglas existentes que admitimos (por ejemplo, Desbordamiento de búfer o Desbordamiento de enteros) para abarcar aún más vulnerabilidades de C/C++ (no solo las que usan específicamente las API de complementos de NodeJS)
Ejecutar estas reglas en el conjunto de datos creado anteriormente
Revisar manualmente los resultados y, si corresponde, crear una prueba de concepto (PoC)
Con este enfoque, pude encontrar varios problemas en paquetes npm al modelar con Snyk Code las API pertinentes de los complementos de NodeJS.
Sin embargo, para algunos de los problemas detectados, seleccioné una muestra de proyectos del conjunto de datos creado y los revisé manualmente.
Resultados
Esta investigación permitió encontrar varias vulnerabilidades en paquetes. Puedes consultarlas a continuación:
Conclusión
En lo personal, esta investigación fue una experiencia de aprendizaje increíble por varias razones. Tuve la oportunidad de profundizar en el mundo de los complementos de NodeJS, revisar la bibliografía existente sobre problemas conocidos e intentar modelar algunas situaciones con Snyk Code para encontrar problemas en un gran conjunto de repositorios.
Aunque conozco bastante bien JavaScript y muchos otros lenguajes, empecé a aprender C/C++ hace poco, gracias al trabajo que hicimos (y seguimos haciendo) para admitir varias reglas de seguridad que ahora están disponibles para los clientes de Snyk Code. Al combinar el aprendizaje con la oportunidad de usar Snyk Code para modelar varios problemas de seguridad, disfruté mucho esta investigación. Por eso, quiero agradecer a Snyk por la oportunidad.
Referencias
Node-API - https://nodejs.org/api/n-api.html
node-addon-api - https://github.com/nodejs/node-addon-api
Complementos C++ - https://nodejs.org/api/addons.html


