Blog AI/ML DevOps Azure Cloud

Compromiso de paquetes AsyncAPI en npm: payloads en tiempo de importación y defensa de la cadena de suministro

Diagrama conceptual de una cadena de suministro npm comprometida con ejecución de código durante la importación de un paquete

El compromiso de paquetes AsyncAPI en npm es relevante no solo por el ecosistema afectado, sino por el patrón de ataque descrito por Microsoft Security: publicación de paquetes comprometidos y entrega de payloads durante la importación del módulo, una ruta de ejecución que muchos equipos tratan como parte normal del desarrollo y de los pipelines.

La diferencia es importante. Un incidente de este tipo no se limita a “he instalado una dependencia vulnerable”. El riesgo aparece cuando un paquete aparentemente legítimo se carga dentro de un proceso Node.js y ejecuta código antes de que la aplicación invoque explícitamente ninguna función exportada. En entornos de CI/CD, esa ejecución puede ocurrir durante tests, generación de documentación, validación de contratos, builds o herramientas de línea de comandos.

Para organizaciones que construyen aplicaciones cloud, plataformas event-driven, herramientas internas o automatizaciones sobre Node.js, el mensaje es claro: la cadena de suministro no debe verse como una lista estática de dependencias, sino como una superficie de ejecución.

Por qué un compromiso npm puede atravesar controles tradicionales

npm es una pieza crítica en muchos entornos de ingeniería. Aunque se asocia principalmente a aplicaciones JavaScript y TypeScript, también aparece en CLIs internas, validadores de contratos, generación de SDKs, documentación viva, herramientas de infraestructura y automatizaciones de despliegue.

AsyncAPI, por su parte, se utiliza en escenarios donde los contratos de eventos y mensajería forman parte del diseño de arquitecturas distribuidas. Por eso, un compromiso en paquetes de este ecosistema puede afectar repositorios que no se perciben como aplicaciones frontend ni como servicios directamente expuestos a Internet.

El problema central es que los gestores de paquetes no solo descargan artefactos. También resuelven árboles complejos de dependencias y, dependiendo del flujo, pueden ejecutar scripts de instalación o cargar código de librerías durante tareas habituales. Si un atacante consigue publicar una versión maliciosa de un paquete confiable, el radio de impacto depende de varios factores:

  • qué proyectos consumen esa versión;
  • si los rangos semánticos permiten actualizar automáticamente;
  • si existe un lockfile efectivo;
  • qué procesos importan el paquete;
  • qué secretos o permisos tiene el entorno donde se ejecuta;
  • si el pipeline separa validación, build, publicación y despliegue.

En una aplicación cloud, el atacante no necesita comprometer frontalmente la API pública si puede insertarse antes, durante la fase de construcción, validación o empaquetado. Por eso, el modelado de amenazas moderno debe incluir dependencias, runners, artefactos, identidades de CI/CD, tokens de publicación y permisos heredados.

La mecánica del payload en tiempo de importación

En Node.js, importar un módulo no es una operación pasiva. Cuando una aplicación ejecuta require("paquete") o una importación ESM como import ... from "paquete", el runtime carga y evalúa el módulo. Si el archivo contiene lógica en el nivel superior, esa lógica se ejecuta al evaluar el módulo, antes de que el consumidor llame explícitamente a una función exportada.

Además, en condiciones normales, Node.js cachea los módulos después de evaluarlos, por lo que el código de nivel superior no se ejecuta en cada llamada a una función, sino durante la carga inicial del módulo en ese proceso. Aun así, esa única ejecución puede ser suficiente si ocurre dentro de un runner con secretos, permisos de publicación o credenciales de cloud.

Este patrón puede ser más difícil de controlar que un script postinstall, porque muchos equipos ya bloquean o auditan scripts de instalación en CI. En cambio, importar módulos forma parte del comportamiento normal de tests, linters, generadores de documentación, validadores de esquemas, builds y CLIs.

El siguiente ejemplo es deliberadamente inocuo. No exfiltra datos ni ejecuta comandos del sistema; solo muestra cuándo se dispara el código.

// archivo: paquete-con-efecto-lateral/index.js

console.log("Código ejecutado durante la importación del módulo");

function validateSchema(schema) {
  if (!schema || typeof schema !== "object") {
    throw new Error("El esquema debe ser un objeto");
  }

  return true;
}

module.exports = {
  validateSchema
};

Si una aplicación importa este paquete, el mensaje aparece antes de llamar a validateSchema.

// archivo: app.js

const { validateSchema } = require("./paquete-con-efecto-lateral");

const schema = {
  asyncapi: "3.0.0",
  info: {
    title: "Orders events",
    version: "1.0.0"
  }
};

console.log("Resultado de validación:", validateSchema(schema));

Al ejecutar node app.js, primero se evalúa el módulo importado y después continúa la lógica de la aplicación. Esa semántica es normal en Node.js, pero se convierte en vector de ataque si el paquete deja de ser confiable.

Advertencia: bloquear únicamente scripts postinstall, preinstall o prepare reduce una clase de riesgo, pero no impide que un paquete ejecute lógica durante la importación en tests, builds o herramientas CLI.

CI/CD: cuando el contexto amplifica el impacto

El componente más delicado de un compromiso de cadena de suministro suele ser el contexto de ejecución. Un portátil de desarrollo puede contener tokens, claves SSH, credenciales npm, sesiones de cloud o archivos .env. Un runner de CI puede tener acceso a secretos de despliegue, registros privados, artefactos firmados, suscripciones cloud o permisos para publicar imágenes de contenedor.

Si el payload se ejecuta dentro de ese contexto, el paquete malicioso no necesita explotar una vulnerabilidad adicional para leer variables de entorno, inspeccionar archivos locales o intentar comunicarse con infraestructura externa. La gravedad dependerá de qué credenciales estaban disponibles y qué acciones podía realizar cada identidad.

En incidentes de este tipo, el objetivo no siempre es comprometer la aplicación final. A veces el objetivo es obtener tokens de publicación, credenciales de cloud, claves de firma o variables de entorno que permitan moverse hacia otros proyectos. Por eso, la respuesta no puede limitarse a “actualizar el paquete afectado”. También hay que revisar la exposición de secretos y permisos durante la ventana de riesgo.

En Azure DevOps, GitHub Actions u otros sistemas de automatización, el principio defensivo es el mismo:

  • los workflows deben ejecutarse con permisos mínimos;
  • los secretos deben estar segmentados por fase;
  • las etapas de validación de código no confiable no deberían tener secretos de producción;
  • publicar paquetes o desplegar infraestructura debe requerir identidades y controles separados;
  • los runners persistentes deben tratarse con especial cuidado tras una posible ejecución maliciosa.

Separar validación, build, firma y despliegue no es burocracia: es una barrera de contención.

Señales de alerta en dependencias npm

No todos los cambios sospechosos son visibles en package.json. Un atacante puede publicar una versión aparentemente menor, mantener la API pública intacta y modificar solo archivos empaquetados. También puede introducir código ofuscado, dependencias nuevas o lógica condicional que se active únicamente en determinados entornos.

En revisiones de emergencia conviene mirar tanto el manifiesto como el tarball real publicado en npm. El repositorio del proyecto puede no coincidir exactamente con el artefacto distribuido. Esa diferencia es importante: muchas organizaciones revisan el código fuente, pero instalan el paquete empaquetado. Si el compromiso ocurre durante la publicación o mediante credenciales de un mantenedor, el tarball puede contener contenido inesperado aunque el repositorio parezca limpio.

Un procedimiento básico de inspección consiste en descargar el artefacto sin instalarlo en el proyecto afectado y revisar su contenido. El siguiente ejemplo usa npm pack con una especificación remota de paquete y versión.

mkdir -p /tmp/npm-review
cd /tmp/npm-review

npm pack @scope/[email protected]

tar -tzf scope-package-name-1.2.3.tgz | sort

El nombre exacto del tarball depende del paquete y de la versión. En paquetes con scope, npm suele generar un archivo con el scope y el nombre normalizados, pero conviene verificar el nombre resultante en cada caso.

Después de listar el contenido, se pueden buscar patrones de riesgo en los archivos JavaScript distribuidos. Este ejemplo usa grep para localizar referencias habituales en payloads que inspeccionan entorno, ejecutan procesos o realizan comunicaciones externas. No es una detección completa, pero ayuda a priorizar una revisión manual.

tar -xzf scope-package-name-1.2.3.tgz

grep -RInE "process\.env|child_process|execSync|spawn|curl|wget|fetch|http\.request|https\.request" package/

El resultado debe interpretarse con contexto. Una CLI legítima puede usar process.env o llamadas HTTP por razones válidas. La señal preocupante aparece cuando esas llamadas se ejecutan en el nivel superior del módulo, están ofuscadas, no están documentadas o se combinan con lectura de secretos y comunicación externa.

Contención inmediata ante una versión comprometida

La respuesta debe equilibrar velocidad y preservación de evidencia. El primer impulso suele ser borrar dependencias y relanzar pipelines, pero eso puede destruir información útil. Si hay sospecha de ejecución maliciosa, es preferible conservar logs, artefactos de build y metadatos del runner antes de limpiar el entorno.

Una secuencia razonable de contención sería:

  1. Congelar temporalmente ejecuciones automáticas que instalen o importen el paquete afectado.
  2. Identificar repositorios, pipelines, imágenes y entornos que resolvieron la versión comprometida.
  3. Revisar qué jobs importaron realmente el paquete, no solo cuáles lo tenían declarado.
  4. Preservar logs, artefactos, hashes, metadatos de runners y eventos de publicación.
  5. Rotar secretos disponibles en esos entornos durante la ventana de exposición.
  6. Invalidar tokens de publicación o despliegue que pudieron estar presentes.
  7. Fijar versiones seguras o aplicar overrides hasta que la dependencia esté corregida.
  8. Revisar artefactos generados durante la ventana, especialmente paquetes publicados e imágenes de contenedor.
  9. Rehabilitar pipelines por fases, con permisos reducidos y observabilidad adicional.

La rotación de secretos debe ser amplia si no se puede demostrar qué leyó el payload. En CI/CD, muchas veces no basta con cambiar un token npm. También deben revisarse credenciales de cloud, tokens del proveedor Git, claves de registros de contenedores, webhooks y credenciales de servicios externos.

Nota: un runner efímero reduce la persistencia local, pero no elimina la exposición de secretos. La pregunta clave no es solo “¿queda malware en el runner?”, sino “¿qué pudo leer o usar mientras se ejecutaba?”.

Endurecimiento de npm en pipelines

La defensa empieza por hacer reproducibles las instalaciones. package-lock.json o npm-shrinkwrap.json reducen la probabilidad de resolver versiones inesperadas, siempre que el pipeline use npm ci en lugar de npm install. npm ci falla si el lockfile no es coherente y evita modificaciones implícitas del árbol de dependencias durante la build.

npm ci --ignore-scripts
npm test

Este patrón instala exactamente lo descrito por el lockfile y evita scripts de instalación. Como se ha explicado, --ignore-scripts no neutraliza payloads en tiempo de importación, pero sí elimina una vía frecuente de ejecución automática durante la instalación.

Para proyectos que necesitan permitir scripts de instalación por dependencias nativas o generación de código, conviene separar esa fase en un entorno sin secretos de producción. Después, los artefactos resultantes pueden promoverse a etapas posteriores mediante controles explícitos.

También es recomendable combinar auditoría de dependencias con políticas de actualización. Las herramientas automáticas que abren pull requests de actualización deben pasar por tests, revisión y análisis, pero no deberían desplegar automáticamente a producción sin intervención. El objetivo no es frenar el mantenimiento, sino evitar que una versión recién publicada llegue a entornos privilegiados sin una ventana mínima de observación.

Controles prácticos para endurecer instalaciones npm:

  • usar npm ci en CI/CD;
  • versionar y revisar lockfiles;
  • evitar rangos demasiado amplios en dependencias críticas;
  • bloquear scripts de instalación cuando sea viable;
  • revisar cambios en dependencias transitivas;
  • ejecutar tests de dependencias en entornos sin secretos sensibles;
  • registrar la versión exacta instalada en cada build;
  • conservar el hash o integridad del artefacto cuando sea posible.

Overrides y bloqueo temporal de versiones

Cuando se identifica una versión maliciosa o sospechosa de una dependencia transitiva, overrides permite forzar una versión concreta en npm. Esto es útil cuando el paquete afectado no está declarado directamente en package.json, sino que llega a través de otra librería.

El siguiente ejemplo es solo ilustrativo. Los nombres y versiones deben sustituirse por los paquetes y versiones confirmados para el incidente concreto.

{
  "name": "orders-service",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "@scope/direct-package": "1.2.3"
  },
  "overrides": {
    "@scope/transitive-package": "4.5.6"
  }
}

No conviene copiar versiones de ejemplo sin contrastarlas con los avisos del mantenedor, el registro npm y fuentes oficiales del incidente.

Advertencia: un override es una medida de contención, no una investigación completa. Si el paquete comprometido ya se ejecutó con secretos disponibles, fijar una versión segura no sustituye la rotación de credenciales ni la revisión de artefactos.

Observabilidad orientada a cadena de suministro

Muchos equipos observan sus aplicaciones en producción, pero no sus pipelines con el mismo nivel de detalle. En incidentes de supply chain, los logs de CI/CD son una fuente primaria: comandos ejecutados, variables enmascaradas, conexiones salientes, artefactos publicados y tiempos de ejecución anómalos pueden revelar qué hizo un payload.

Una estrategia madura incluye:

  • eventos de instalación de dependencias;
  • versiones exactas instaladas;
  • hashes de artefactos;
  • procedencia de imágenes;
  • firma de paquetes o artefactos cuando aplique;
  • relación entre commit, build, artefacto y despliegue;
  • auditoría de uso de secretos;
  • registros de publicación en npm, registros de contenedores y repositorios internos.

En Azure, esta trazabilidad puede integrarse con los sistemas de logging y seguridad que la organización ya utilice, como registros centralizados, Microsoft Defender for Cloud o Microsoft Sentinel. Lo importante no es generar alertas genéricas, sino poder responder con precisión a preguntas concretas:

  • ¿qué versión se instaló?
  • ¿en qué pipeline se ejecutó?
  • ¿qué identidad tenía el job?
  • ¿qué secretos estaban disponibles?
  • ¿qué artefactos se generaron?
  • ¿qué conexiones salientes se observaron?
  • ¿qué paquetes o imágenes se publicaron después?

Sin esa trazabilidad, la respuesta queda reducida a suposiciones.

Qué revisar en arquitecturas cloud con AsyncAPI

AsyncAPI suele aparecer en plataformas event-driven, integraciones con brokers, documentación de contratos y generación de clientes o servidores. Por eso, el impacto de un paquete comprometido puede concentrarse en herramientas de desarrollo más que en servicios runtime. Aun así, esas herramientas pueden estar conectadas a repositorios, registros y pipelines con permisos significativos.

En una arquitectura sobre Azure o cualquier otra plataforma cloud, conviene revisar especialmente los puntos donde los contratos AsyncAPI se procesan automáticamente:

  • validación de especificaciones al abrir un pull request;
  • generación de documentación interna;
  • generación de SDKs o stubs;
  • publicación de paquetes derivados;
  • creación de imágenes de contenedor;
  • despliegues automáticos asociados a cambios de contrato;
  • herramientas CLI ejecutadas por desarrolladores.

Un pipeline que valida especificaciones en una pull request no debería tener secretos de despliegue. Una tarea que genera documentación no necesita acceso a credenciales de producción. Un job que publica SDKs generados debería estar separado del job que procesa contenido no confiable.

También hay que considerar los entornos de desarrollador. Si una herramienta comprometida se ejecutó localmente, los secretos en .env, credenciales cacheadas de Azure CLI, tokens npm, claves SSH o sesiones del proveedor Git pueden haber estado expuestos. El plano local no debe tratarse como irrelevante solo porque el despliegue final ocurra en cloud.

Lecciones para equipos de plataforma

La lección principal del compromiso de AsyncAPI en npm es que la cadena de suministro debe tratarse como una superficie de ejecución. Cada instalación, importación, test y generación de artefactos puede ejecutar código. Que ese código provenga de un paquete conocido reduce fricción operativa, pero no elimina el riesgo.

Los equipos de plataforma pueden convertir este tipo de incidente en mejoras concretas:

  1. Separar pipelines por nivel de confianza.
  2. Ejecutar validación de código no confiable sin secretos sensibles.
  3. Aplicar permisos mínimos a tokens, service principals e identidades federadas.
  4. Hacer reproducibles las builds con lockfiles y comandos deterministas.
  5. Registrar procedencia, versiones y hashes de artefactos.
  6. Revisar dependencias transitivas, no solo directas.
  7. Evitar despliegues automáticos desde actualizaciones recién publicadas.
  8. Practicar ejercicios de respuesta ante dependencias comprometidas.
  9. Mantener inventario de dependencias críticas y responsables internos.
  10. Definir de antemano qué secretos rotar ante una exposición de CI/CD.

La disciplina clave es limitar el radio de impacto. Una dependencia comprometida puede llegar a ejecutarse, pero no debería tener acceso automático a todos los secretos, permisos y artefactos de la organización.

Checklist práctico de revisión

Este checklist resume controles que un equipo puede aplicar a corto plazo:

  1. Verificar que los pipelines usan npm ci y lockfiles versionados.
  2. Activar --ignore-scripts donde sea compatible con el proyecto.
  3. Separar jobs de validación, build, publicación y despliegue.
  4. Reducir permisos por defecto de tokens en GitHub Actions, Azure DevOps u otros sistemas CI/CD.
  5. Ejecutar validaciones de pull request sin secretos de producción.
  6. Revisar dependencias que ejecutan código durante importación en herramientas críticas.
  7. Registrar versiones instaladas y hashes de artefactos en cada build.
  8. Definir un procedimiento de rotación de secretos para incidentes de supply chain.
  9. Auditar runners persistentes tras cualquier sospecha de ejecución maliciosa.
  10. Preferir runners efímeros para código no confiable.
  11. Mantener inventario de dependencias críticas.
  12. Probar overrides o bloqueos temporales de versiones antes de necesitarlos.
  13. Revisar artefactos publicados durante la ventana de exposición.
  14. Comparar el repositorio fuente con el tarball publicado cuando exista sospecha.
  15. Conservar logs antes de limpiar entornos afectados.

Este checklist no sustituye un programa formal de seguridad de cadena de suministro, pero establece una base operativa. La diferencia entre un incidente contenido y uno sistémico suele estar en decisiones previas: qué secretos estaban disponibles, qué permisos tenía el pipeline y qué evidencia quedó registrada.

Conclusión

El compromiso de paquetes AsyncAPI en npm subraya una realidad incómoda: las dependencias no son únicamente código que enlazamos, sino código que ejecutamos en momentos sensibles del ciclo de vida. La entrega de payloads en tiempo de importación aprovecha una característica normal de Node.js y la combina con la confianza depositada en paquetes legítimos y flujos CI/CD automatizados.

La respuesta eficaz no pasa por desconfiar de todo indiscriminadamente, sino por diseñar contención. Lockfiles, instalaciones reproducibles, permisos mínimos, segmentación de secretos, revisión de artefactos y observabilidad de pipelines reducen el radio de impacto cuando una dependencia falla.

En arquitecturas cloud modernas, especialmente aquellas que combinan APIs, eventos, generación de código y automatización intensiva, esta disciplina ya no es opcional: forma parte de la seguridad básica de la plataforma.