Skip to main content

Dentro del compromiso de keyv en npm: malware preinstall, procedencia confiable y hooks de IDE

feature insights context

4 de agosto de 2026

0 minutos de lectura

El 4 de agosto de 2026, atacantes comprometieron la ruta de publicación de keyv y otros paquetes de npm relacionados. Las versiones maliciosas agregan un hook preinstall que ejecuta un cargador ofuscado antes de que se inicie el código de la aplicación y, luego, lanza una carga útil de segunda etapa mucho más grande.

Se trata de un incidente activo en la cadena de suministro de software, no de una prueba de concepto. Snyk Security Research descargó y comparó de forma independiente los tarballs publicados sin instalarlos, enumeró todos los paquetes que npm devolvió para el mantenedor jaredwray e identificó 11 versiones maliciosas con los mismos dos archivos de carga útil. A las 11:16 UTC, ocho de esas versiones todavía tenían la etiqueta latest.

Resumen

  • Incidente: Código malicioso integrado en paquetes legítimos de npm

  • Paquete principal: keyv@6.0.0

  • Versiones afectadas detectadas por Snyk: 11 en keyv, paquetes relacionados con cacheable y ecto

  • Ejecución: "preinstall": "node setup.mjs"

  • Carga útil: setup.mjs carga una segunda etapa de 727,680 bytes llamada Math_Symbol.js

  • Estado observado a las 11:16 UTC: Ocho versiones maliciosas seguían con la etiqueta latest. Se habían eliminado tres.

  • Aviso: SNYK-JS-KEYV-18515941

  • CVE/GHSA: Al momento de la investigación, no se había asignado ningún CVE ni GHSA

  • Severidad operativa: Crítica, porque la instalación permite ejecutar código controlado por el atacante con los privilegios del desarrollador o del ejecutor de CI

  • Remediación: Fija la versión o vuelve a una versión anterior que se sepa que es segura. No se había publicado ninguna versión sucesora segura de keyv@6.0.0.

  • Acción inmediata: No instales las versiones afectadas. Si ejecutaste una, aísla el host, busca y deshabilita la persistencia y, luego, rota las credenciales expuestas desde un sistema limpio.

Severidad y estado del aviso

Snyk publicó SNYK-JS-KEYV-18515941 mientras preparábamos este borrador. El aviso clasifica keyv@6.0.0 como código malicioso integrado, según CWE-506.

Durante nuestra investigación, no se había asignado ningún CVE ni registro de GitHub Advisory. La instalación afectada ejecuta código controlado por el atacante sin necesidad de privilegios adicionales ni interacción con la aplicación, usando los permisos y las credenciales disponibles para el desarrollador o el proceso de CI.

Qué confirmó Snyk de forma independiente

Consultamos el endpoint de búsqueda del registro de npm para los paquetes mantenidos por jaredwray. El endpoint devolvió 61 nombres de paquetes. Para cada nombre, recuperamos el manifiesto del registro, examinamos todas las versiones publicadas el 4 de agosto y verificamos sus scripts de ciclo de vida y los metadatos de los tarballs.

La revisión detectó estas versiones maliciosas y confirmó que los demás paquetes del ámbito @keyv no se habían comprometido:

La última entrada es importante para delimitar el alcance del incidente. ecto@5.0.1 apareció después de las primeras alertas públicas, y sus archivos setup.mjs y Math_Symbol.js son idénticos byte por byte a los de keyv@6.0.0. Las primeras listas de paquetes que omiten ecto están incompletas.

En nuestra captura de las 11:16 UTC, npm había eliminado flat-cache@6.1.24, cacheable-request@13.0.20 y cache-manager@7.2.10, y había vuelto a asignar sus etiquetas latest a 6.1.23, 13.0.19 y 7.2.9. Las otras ocho versiones afectadas seguían disponibles y tenían la etiqueta latest. El estado del registro puede cambiar rápido durante un incidente activo, por lo que los mirrors privados y los archivos de bloqueo siguen siendo parte de la investigación incluso después de que npm elimina una versión.

El tarball publicado difiere en tres puntos

Descargamos keyv@6.0.0 y su versión candidata, keyv@6.0.0-rc.1, directamente del registro y comparamos todos los archivos mediante SHA-256. No instalamos el paquete ni ejecutamos ninguna de las dos cargas útiles.

Solo hay tres rutas diferentes:

  • package.json cambió de la versión 6.0.0-rc.1 a la 6.0.0, agregó los dos archivos de carga útil a la lista de archivos publicados y agregó el script preinstall.

  • Se agregó setup.mjs con un tamaño de 29,918 bytes.

  • Se agregó Math_Symbol.js con un tamaño de 727,680 bytes.

Todos los archivos de dist/ son idénticos byte por byte entre la versión candidata y la versión estable comprometida. La biblioteca sigue funcionando con normalidad después de la instalación, mientras que el hook de ciclo de vida se ejecuta por separado. Esa pequeña diferencia es una pista útil para la detección y una técnica eficaz para ocultarse.

El cambio en el manifiesto es directo:

 "scripts": {
   "build": "tsdown",
+  "preinstall": "node setup.mjs"
 },
 "files": [
   "dist",
   "LICENSE",
+  "setup.mjs",
+  "Math_Symbol.js"
 ]

El manifiesto de keyv@6.0.0 en el registro informa el siguiente valor de integridad del tarball:

sha512-N/n4R+nD5SC0fYOpAp4ZnbwwxqGVodgEZ9D7Gm/VBocorU0aQimVyleDWSY6/axdO0/temub760n3hnMppZpUg==

Nuestros hashes independientes son:

keyv-6.0.0.tgz
sha256 d584f9b6af48b7ed1f93713944f033783bf149e1c25e1643eb8c0e9df5dc7782

setup.mjs, 29,918 bytes
sha256 54dc7ea54a1317cca0e890a2770630cf7fa6c97813e0cb9d2caa93012b350668

Math_Symbol.js, 727,680 bytes
sha256 9fc2570b7cef51c1b8df116d144d11ff4096357be7d2c4c6367cfc2509cf1bcc

Luego repetimos la verificación de hashes de la carga útil para cada versión afectada que pudimos recuperar durante nuestro análisis inicial. Los nueve tarballs disponibles en ese momento contenían el mismo archivo setup.mjs de 29,918 bytes y el mismo archivo Math_Symbol.js de 727,680 bytes, con exactamente los hashes indicados arriba. Más tarde, npm eliminó una de esas versiones. Para detectar el problema, usa los hashes y el comportamiento del ciclo de vida, en lugar de depender de un único nombre de paquete.

Cómo se ejecuta el malware

npm ejecuta automáticamente preinstall durante la instalación de dependencias. El desarrollador no necesita importar keyv, iniciar una aplicación ni llamar a una API vulnerable. Basta con resolver e instalar el paquete afectado.

El análisis estático de setup.mjs muestra verificaciones de plataforma para Linux, macOS y Windows, uso de child_process.execFileSync, operaciones del sistema de archivos, acceso HTTPS y manejo del entorno de ejecución de Bun. El cargador menos ofuscado, almacenado en .claude/setup.mjs, identifica la versión 1.3.13 de Bun y el nombre del archivo de segunda etapa, math_init.js. El cargador puede descargar desde GitHub una versión adecuada de Bun si no está instalado, ejecutar la carga útil más grande y eliminar los artefactos temporales del entorno de ejecución.

El análisis independiente del malware incluido en los informes del incidente indica que la segunda etapa cifrada apunta a tokens de GitHub y npm, credenciales de la nube, claves privadas, cadenas de conexión a bases de datos, tokens de Vault, tokens de cuentas de servicio de Kubernetes y memoria de los ejecutores de GitHub Actions. También informa sobre un mecanismo de persistencia, gh-token-monitor, que monitorea un token de GitHub robado y ejecuta un handler proporcionado cuando el token deja de funcionar.

No ejecutamos ni desciframos de forma independiente Math_Symbol.js, por lo que esos detalles sobre las capacidades de la segunda etapa deben conservar esa atribución. La guía operativa coincide con el análisis previo de Snyk sobre el mismo patrón de persistencia gh-token-monitor: contiene y deshabilita el monitor antes de revocar el token.

Una segunda vía de ejecución apunta a las herramientas de desarrollo

El hook del ciclo de vida de npm es solo un punto de entrada. Un registro de la API de GitHub del commit d8c850c7 muestra que se agregaron cinco archivos al repositorio de keyv:

  • .claude/settings.json

  • .claude/setup.mjs

  • .claude/math_init.js

  • .vscode/tasks.json

  • .vscode/setup.mjs

La configuración de Claude registra un comando SessionStart que invoca .vscode/setup.mjs. La tarea de VS Code usa runOn: "folderOpen" e invoca .claude/setup.mjs. Estos archivos crean una vía de ejecución adicional cuando un desarrollador o agente de programación confía en la configuración local del proyecto y la activa. Según la confianza del espacio de trabajo y la configuración del usuario, VS Code puede pedir confirmación antes de permitir tareas automáticas; por eso, abrir un checkout indica exposición, pero no demuestra que se haya ejecutado código.

GitHub verificó criptográficamente el commit, cuya identidad de autor es github-actions[bot]. La insignia de verificación demuestra que GitHub firmó el objeto del commit. No demuestra que el cambio haya sido autorizado por el mantenedor del proyecto. La evidencia respalda que se comprometió una cuenta, credencial, sesión o ruta de publicación. No identifica a la persona que la operó, y se debe tratar al mantenedor como una víctima del incidente.

La procedencia válida firmó la versión maliciosa

El manifiesto de npm identifica a GitHub Actions como el publicador de confianza de keyv@6.0.0 y enlaza a una atestación de npm para la versión. El código fuente malicioso estaba presente en el estado etiquetado del repositorio, por lo que el flujo de trabajo legítimo compiló y atestiguó el artefacto malicioso.

El parche del commit de publicación también agregó una prueba que ejecutaba setup.mjs mediante execFileSync. Un commit posterior eliminó solo esa prueba. Por lo tanto, ejecutar el conjunto de pruebas de la versión podría haber ejecutado el malware en CI antes de la publicación.

La procedencia sigue siendo una evidencia valiosa del origen de la compilación. Este incidente muestra sus límites: la procedencia puede atestiguar fielmente una compilación cuyo código fuente o contexto del flujo de trabajo ya se haya comprometido.

Impacto y posible alcance

Los principales paquetes afectados tienen volúmenes de instalación muy altos. npm registró 619,682,667 descargas de keyv, 579,751,309 de flat-cache y 571,240,025 de file-entry-cache entre el 5 de julio y el 3 de agosto.

Estas cifras se superponen ampliamente porque los paquetes dependen unos de otros y aparecen en las mismas cadenas de herramientas. Miden el alcance en el ecosistema, no la cantidad de hosts comprometidos. Además, el período de exposición de cada versión maliciosa fue mucho menor que un mes.

Snyk observa un gran alcance en los proyectos monitoreados. El uso transitivo es importante porque flat-cache y file-entry-cache suelen incorporarse a través de herramientas de desarrollo como ESLint. Las laptops de desarrolladores y los ejecutores de CI son objetivos especialmente valiosos porque a menudo contienen credenciales de GitHub, npm, servicios en la nube, firma y despliegue.

Al momento de redactar este texto, no hay un recuento público verificado de las ejecuciones exitosas de la segunda etapa ni de las credenciales exfiltradas. Los paquetes se publicaron activamente en el registro de producción de npm y, en nuestra captura, ocho seguían con la etiqueta latest, lo que confirma una distribución real y no un exploit de laboratorio. El aviso de Snyk clasifica la madurez del exploit como Atacado, pero no publica la cantidad de víctimas.

Cómo detectar la exposición

Primero, inspecciona el árbol de dependencias resuelto, incluidas las dependencias transitivas:

npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Busca en los archivos de bloqueo, ya que una versión afectada puede seguir fijada después de que npm cambie una dist-tag:

rg -n \
  'keyv|flat-cache|file-entry-cache|cacheable-request|cacheable|cache-manager|@cacheable/|ecto' \
  package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock

Inspecciona los manifiestos instalados para encontrar el hook exacto, sin ejecutar el código del paquete:

find node_modules -name package.json -print0 |
  xargs -0 node -e '
    const fs = require("node:fs");
    for (const file of process.argv.slice(1)) {
      try {
        const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
        if (pkg.scripts?.preinstall === "node setup.mjs") {
          console.log(`${pkg.name}@${pkg.version} ${file}`);
        }
      } catch {}
    }
  '

Busca indicadores de la carga útil y de persistencia:

find "$HOME" /tmp \
  \( -name setup.mjs -o -name Math_Symbol.js -o -name math_init.js \
     -o -name gh-token-monitor.sh -o -name gh-token-monitor.service \
     -o -name com.user.gh-token-monitor.plist \) \
  -print 2>/dev/null

También inspecciona los repositorios a los que se pueda acceder con credenciales expuestas de GitHub para encontrar archivos .claude/settings.json o .vscode/tasks.json inesperados, archivos de flujo de trabajo que contengan toJSON(secrets) y artefactos de GitHub Actions creados recientemente.

Los clientes de Snyk pueden usar el nuevo aviso sobre keyv@6.0.0 y deben volver a ejecutar las pruebas de dependencias y monitorear los proyectos a medida que se actualiza la información sobre el incidente:

snyk test --all-projects
snyk monitor --all-projects

La lección de Snyk Learn sobre paquetes legítimos comprometidos ofrece más contexto para integrar la integridad de las dependencias y los controles del registro en los flujos de trabajo de desarrollo habituales.

Remediación

Si se instaló un paquete afectado

Trata la máquina o el ejecutor como potencialmente comprometido, aunque ya hayas eliminado node_modules.

  1. Aísla el host del acceso normal a la red. Conserva los registros, los datos de procesos, los logs de npm, la salida de los trabajos de CI y las marcas de tiempo del sistema de archivos para la investigación.

  2. Busca la persistencia de gh-token-monitor antes de revocar las credenciales de GitHub. Revisa ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor/, ~/.config/systemd/user/gh-token-monitor.service y ~/Library/LaunchAgents/com.user.gh-token-monitor.plist.

  3. Deshabilita cualquier mecanismo de persistencia que encuentres. Coordina su eliminación con el equipo de respuesta a incidentes y conserva una copia forense. El monitor podría detectar la revocación.

  4. Rota las credenciales desde un sistema que sepas que está limpio. Incluye tokens de acceso personal (PAT) y tokens de aplicaciones de GitHub, tokens de npm, AWS, GCP, Azure, Vault, Kubernetes, credenciales de bases de datos, claves privadas y cualquier secreto disponible para los trabajos de CI afectados.

  5. Revisa los registros de auditoría de las cuentas y la nube. Busca publicaciones inesperadas en npm, creación de repositorios, modificaciones de flujos de trabajo, artefactos de Actions, llamadas a API de la nube y uso de tokens desde ubicaciones desconocidas.

  6. Elimina los artefactos afectados de los registros privados y las cachés. Eliminar un paquete de npm no borra las copias que ya estén almacenadas en un proxy interno o en la caché de un desarrollador.

Fija versiones que sepas que están limpias antes de volver a instalar

Las siguientes versiones anteriores no tenían ningún hook del ciclo de vida de instalación en sus manifiestos del registro cuando las revisamos:

{
  "overrides": {
    "keyv": "5.6.0",
    "flat-cache": "6.1.23",
    "file-entry-cache": "11.1.5",
    "cacheable-request": "13.0.19",
    "cacheable": "2.5.0",
    "@cacheable/utils": "2.5.0",
    "cache-manager": "7.2.9",
    "@cacheable/net": "2.1.0",
    "@cacheable/node-cache": "3.1.1",
    "@cacheable/memory": "2.2.0",
    "ecto": "5.0.0"
  }
}

keyv@5.6.0 es la última versión estable y limpia de 5.x en nuestra captura. Volver de keyv@6.0.0 a 5.x puede requerir cambios en el código. 6.0.0-rc.1 tenía una salida dist/ idéntica byte por byte y no incluía un hook del ciclo de vida, pero los equipos de producción deberían preferir una versión estable consolidada, salvo que hayan validado explícitamente la versión candidata.

Después de agregar las sobrescrituras, vuelve a generar el archivo de bloqueo sin ejecutar scripts del ciclo de vida:

npm install --package-lock-only --ignore-scripts
rm -rf node_modules
npm cache clean --force
npm ci --ignore-scripts
npm ls keyv flat-cache file-entry-cache cacheable-request cacheable \
  @cacheable/utils cache-manager @cacheable/net \
  @cacheable/node-cache @cacheable/memory ecto --all

Deshabilitar los scripts del ciclo de vida limita esta vía de ejecución. Algunos paquetes legítimos requieren scripts de instalación, por lo que los equipos deberían mantener una lista de permitidos acotada en lugar de habilitar los scripts de forma global. Las prácticas recomendadas de seguridad para npm de Snyk explican con más detalle las instalaciones deterministas, los controles de scripts y la revisión de paquetes.

Cronología del incidente

Todas las horas que se indican a continuación corresponden al 4 de agosto de 2026, en UTC.

  • 09:02 a 09:17: El commit ee2681a9 prepara keyv@6.0.0, agrega el hook del ciclo de vida, los archivos de carga útil y una prueba que ejecuta el cargador. GitHub registra una hora de autoría de las 09:02 y una hora de confirmación de las 09:17.

  • 09:04: El commit verificado d8c850c7 agrega los hooks de ejecución de Claude y VS Code.

  • 09:23: El commit f97eabcd elimina la prueba de preinstalación.

  • 09:30 a 09:32: Se publican varias versiones 6 de @keyv/* sin el hook malicioso.

  • 09:35: npm publica keyv@6.0.0 con el hook malicioso.

  • 09:51: El commit 1f79edd8 agrega los archivos de carga útil en los espacios de trabajo de @keyv/*, lo que crea un riesgo para las publicaciones posteriores.

  • 09:49 a 09:51: Los problemas de GitHub #2044, #2045 y #2046 reportan el incidente. Más tarde, la API de problemas devolvió 410 Gone.

  • 10:09 a 10:14: Se publican las versiones maliciosas de la familia Cacheable.

  • 10:18 y 10:20: El investigador de seguridad Charlie Eriksen publica las dos alertas públicas proporcionadas.

  • 10:28: ecto@5.0.1 se publica con la misma carga útil, lo que amplía a 11 la lista de paquetes vinculados al mantenedor.

  • 10:39: Cambian los metadatos del registro de npm después de que se elimina cacheable-request@13.0.20.

  • 10:42: Cambian los metadatos del registro de npm después de que se elimina flat-cache@6.1.24.

  • 11:11: Cambian los metadatos del registro de npm después de que se elimina cache-manager@7.2.10.

  • 11:16: La captura del registro de Snyk todavía detecta ocho versiones maliciosas con la etiqueta latest.

  • Durante la redacción: Snyk publica SNYK-JS-KEYV-18515941 y cerca de 70 avisos más.

Esta cronología refleja un incidente en curso. Vuelve a revisar los manifiestos de npm, las etiquetas dist-tag y los avisos de Snyk justo antes de la publicación.

Webinar a pedido

OpenAI se calificó a sí misma y luego vulneró producción

Mira la grabación a pedido para descubrir por qué la autovalidación falla por diseño, por qué una pila multimodelo empeora el problema y cómo es la validación independiente en la práctica. Llévate un marco para gobernar todos los activos de IA de tu entorno, sin importar qué laboratorio los haya creado.