Un paquete malicioso no necesita un exploit ingenioso. Solo necesita instalarse una vez, en un portátil o en un runner de build, por alguien que confía en el registry. Desde ahí puede leer variables de entorno, tokens y claves SSH, y enviarlos a otro sitio.
Solo hay un puñado de formas de entrar. Conocerlas hace obvias las defensas.
La cadena
Casi todos los casos siguen la misma forma, sea cual sea el ecosistema:
- PublicarSe publica un paquete, o uno existente recibe una versión nueva, con un payload dentro.
- Ser elegidoUn desarrollador, una actualización del lockfile o un resolver lo recoge.
- Ejecutar en la instalaciónUn lifecycle script o un fichero de setup se ejecuta durante la instalación, con los permisos del usuario.
- RecolectarEl payload recoge tokens, credenciales cloud y variables de entorno.
- PropagaciónSe usan tokens de publicación robados para subir versiones infectadas de otros paquetes.
El último paso no es hipotético. El gusano Shai-Hulud de npm, en septiembre de 2025, hizo exactamente esto: usó tokens robados para republicar otros paquetes que mantenían sus víctimas.
Puerta uno: typosquats
El atacante publica un nombre parecido al de uno popular: una letra cambiada, un guion que falta, un sufijo plausible como -utils o -cli. El paquete suele funcionar, envolviendo o reexportando el real, así que nada se rompe y nadie mira dos veces.
Cómo se ve: una dependencia que no reconoces en un diff, con pocas descargas, una primera publicación reciente y una descripción copiada del original. Revisa las nuevas dependencias directas cuando se añaden, no meses después.
Los typosquats llegan cada vez más por sugerencias en lugar de por errores al teclear. Un snippet copiado de un foro, un tutorial desfasado o un asistente de código pueden nombrar un paquete que suena bien pero no existe, y los atacantes vigilan esos nombres y los registran. El hábito que ayuda es el mismo en ambos casos: antes de añadir una dependencia, abre su página del registry y su repositorio de código, y comprueba que coinciden y que el proyecto tiene historial.
Puerta dos: scripts de instalación
npm ejecuta por defecto los scripts preinstall, install y postinstall. Las distribuciones fuente de Python pueden ejecutar código arbitrario en setup.py. Es decir, el payload se ejecuta antes de que lo vea ningún test, linter o revisión de código.
{
"name": "left-padder",
"version": "1.0.3",
"scripts": {
"postinstall": "node ./lib/telemetry.js"
}
}
Un script llamado telemetry.js o setup.js que está minificado, codificado o descarga una segunda fase desde la red es la señal clásica. Puedes listar qué paquetes instalados declaran un script postinstall:
npm query ":attr(scripts, [postinstall])" | jq -r '.[].name'
La mayoría de los proyectos tiene una lista corta de paquetes que de verdad necesitan compilar algo de forma nativa. Todo lo demás de esa lista merece una mirada.
Puerta tres: secuestro de maintainer
Este es el más difícil de detectar, porque el nombre del paquete es el correcto. Un atacante hace phishing a un maintainer, reutiliza un token filtrado o recibe la propiedad de un proyecto abandonado, y publica una nueva versión de parche. Tu rango de semver la acepta automáticamente.
Cómo se ve: una versión de parche de un paquete inactivo desde hace tiempo, un nuevo publisher, un script de instalación que antes no estaba o una versión sin tag correspondiente en el repositorio de código. Las attestations de provenance, donde el ecosistema las admite, ayudan aquí: una versión construida fuera del pipeline habitual del proyecto no llevará una.
Puerta cuatro: dependency confusion
Tu organización tiene un paquete interno, por ejemplo acme-billing-utils, en un registry privado. Un atacante publica el mismo nombre en el registry público con un número de versión más alto. Si tu tooling mira ambos registries y elige la versión más alta, instala la suya.
La clásica mala configuración en Python es --extra-index-url, que fusiona índices en vez de preferir uno:
# risky: pip may resolve from either index
pip install --extra-index-url https://pypi.internal.example/simple acme-billing-utils
# safer: one index, which proxies the public one
pip install --index-url https://pypi.internal.example/simple acme-billing-utils
En npm, pon los paquetes internos bajo un scope y asocia ese scope a tu registry, para que nunca se consulte el registry público:
# .npmrc
@acme:registry=https://npm.internal.example/
Compruébalo en 10 minutos
- Ejecuta el
npm queryde arriba en tu repositorio más grande y lee la lista. - Busca
extra-index-urlen la configuración de CI y en la documentación para desarrolladores. - Lista los nombres de paquetes internos y confirma que cada uno tiene scope o está reservado en el registry público.
- Comprueba qué tokens de publicación existen para tus propios paquetes, quién los tiene y si tienen scope limitado y vida corta.
- Mira los diffs del lockfile del último mes: ¿una persona revisó las nuevas dependencias directas?
Ninguno de estos necesita un producto. Necesitan que alguien mire, una vez, y luego una puerta para no tener que mirar a mano otra vez.