Seguridad de la cadena de suministro en npm y PyPI: controles concretos después de Shai-Hulud
En septiembre de 2025, CISA publicó una alerta sobre un gusano autorreplicante, conocido como "Shai-Hulud", que había comprometido más de 500 paquetes de npm. El malware buscaba credenciales en el entorno donde se instalaba: tokens personales de GitHub y claves de API de AWS, Google Cloud y Azure. Las exfiltraba y después usaba la identidad del desarrollador comprometido para autenticarse en npm, inyectar código en otros paquetes y publicar versiones maliciosas. Así se propagaba solo.
El caso resume el problema de la cadena de suministro de software: el código que corre en tus laptops, en tu CI y en producción incluye cientos de paquetes que nadie del equipo revisó, y basta con que un mantenedor pierda sus credenciales para que una versión maliciosa llegue a miles de proyectos en horas. Esta guía reúne controles concretos para npm y PyPI, todos basados en documentación oficial.
Dos lados del problema: consumir y publicar
Hay dos superficies distintas:
- Como consumidor: evitar instalar una versión maliciosa y limitar el daño si ocurre.
- Como publicador: evitar que alguien publique en tu nombre si te roban credenciales.
La mayoría de los equipos solo piensa en la primera, pero cualquier empresa que publique un SDK, una CLI o una librería interna en un registro público está en la segunda.
Consumir: instalaciones reproducibles
La base es que lo que se instala en CI y producción sea exactamente lo que se revisó. Eso significa lockfile versionado en el repo e instalación estricta:
# npm: instala exactamente lo que dice package-lock.json y falla si no coincide con package.json
npm ci
# Python con pip: exigir hashes para cada paquete
pip install --require-hashes -r requirements.txt
# Python con uv: fallar si el lockfile quedaría desactualizado
uv sync --locked
CISA recomendó justamente revisar los lockfiles (package-lock.json, yarn.lock) para identificar paquetes afectados, incluidos los transitivos, y fijar versiones conocidas como seguras. Sin lockfile, esa revisión es imposible.
Consumir: no instalar lo que se publicó hace cinco minutos
Una versión maliciosa solo puede retirarse después de que alguien la detecta, y en ese intervalo cualquier instalación la descarga. Esperar unos días antes de adoptar versiones nuevas reduce esa exposición, y hoy las principales herramientas lo soportan de forma nativa:
# .npmrc: solo instalar versiones publicadas hace más de 7 días
min-release-age=7
# excepción para paquetes internos
min-release-age-exclude[]=@miempresa/*
# pyproject.toml con uv: ignorar artefactos subidos en los últimos 7 días
[tool.uv]
exclude-newer = "7 days"
En npm, min-release-age se expresa en días. Existe también before, que acepta una fecha exacta. Un detalle a tener en cuenta: si la ventana bloquea una corrección de seguridad que npm audit fix querría instalar, npm mantiene la versión vulnerable, avisa y termina con error. Para esos casos está min-release-age-exclude. pnpm tiene una opción equivalente, minimumReleaseAge; revisá su documentación para la unidad y las exclusiones.
Consumir: controlar los scripts de instalación
Los scripts preinstall, install y postinstall son uno de los vectores clásicos: se ejecutan con los permisos de quien instala, en su máquina o en el runner de CI. Opciones en npm:
ignore-scripts=true: no ejecuta scripts de ciclo de vida de las dependencias. Es la opción más estricta, pero algunos paquetes con binarios nativos la necesitan.- Política
allowScriptsenpackage.json: lista explícita de paquetes cuyos scripts se permiten o se niegan.npm approve-scriptsayuda a revisar los pendientes. strict-allow-scripts=true: convierte la política en error. Cualquier dependencia con scripts no revisados hace fallar la instalación, en lugar de ejecutarse con un aviso.
Una combinación razonable para equipos: política allowScripts versionada en el repo y strict-allow-scripts activado en CI.
Consumir: verificar firmas y limitar credenciales
# Verifica las firmas del registro de los paquetes instalados (npm >= 8.15.0)
npm audit signatures
Además, el daño de un paquete malicioso depende de qué encuentre al ejecutarse. Shai-Hulud buscaba tokens de GitHub y claves de nube. Por eso:
- Los runners de CI no deberían tener credenciales de producción de larga duración en variables de entorno durante el paso de instalación.
- Las laptops de desarrollo no deberían tener claves de nube con permisos amplios guardadas en texto plano.
- Preferí credenciales de corta duración (OIDC entre tu CI y tu nube) a secretos estáticos.
Publicar: trusted publishing en lugar de tokens
Si tu organización publica paquetes, el control más importante es dejar de usar tokens de larga duración para publicar. Tanto npm como PyPI soportan trusted publishing con OIDC: el registro confía en una identidad de tu CI (repositorio y workflow concretos) y emite credenciales efímeras en cada ejecución. En PyPI, el token que se obtiene es válido solo por 15 minutos.
En npm, trusted publishing requiere npm CLI 11.5.1 o superior y Node 22.14.0 o superior. Hoy soporta GitHub Actions, GitLab CI/CD y CircleCI, solo con runners en la nube, y hasta 10 trusted publishers por paquete. Con GitHub Actions o GitLab, npm genera automáticamente atestaciones de provenance. Un workflow mínimo en GitHub Actions:
name: publish
on:
release:
types: [published]
permissions:
id-token: write # necesario para OIDC
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- run: npm ci
- run: npm publish
Después, en la configuración del paquete en npmjs.com, se registra ese repositorio y archivo de workflow como trusted publisher. Una vez que funcione, revocá los tokens de publicación viejos.
Publicar: una aprobación humana antes de salir
npm agregó staged publishing: en lugar de publicar directamente, el paquete queda en una etapa intermedia y un mantenedor debe aprobarlo con 2FA. Requiere npm CLI 11.15.0 o superior, y el paquete tiene que existir previamente en el registro.
npm stage publish # sube la versión a la etapa (no requiere 2FA)
npm stage list # lista versiones pendientes
npm stage approve <id> # la publica, con verificación 2FA
Combinado con trusted publishing, el CI puede preparar la versión, pero publicarla requiere una persona con segundo factor. Es exactamente el tipo de control que corta la propagación automática de un gusano que usa credenciales robadas.
Qué hacer con esto
Para todos los equipos
- Lockfile versionado e instalación estricta en CI (
npm ci,uv sync --locked,--require-hashes). - Edad mínima de versiones (
min-release-age,exclude-newer) con excepciones para paquetes internos. - Política de scripts de instalación (
allowScripts+strict-allow-scripts, oignore-scripts). -
npm audit signaturesen el pipeline. - Nada de credenciales de producción de larga duración en el paso de instalación.
- MFA resistente a phishing en GitHub, npm y PyPI, como recomienda CISA.
Si publicás paquetes
- Migrar a trusted publishing (OIDC) y revocar tokens de publicación.
- Activar staged publishing para exigir aprobación con 2FA.
- Mantener provenance activado.
¿Cuántos días pasan hoy entre que se publica una versión nueva de una dependencia y que llega a tu entorno de producción?
Lecturas relacionadas
- uv para Python: guía práctica para reemplazar pip, venv y pip-tools en tu equipo: entornos, lockfile, versiones de Python y herramientas en un solo binario, con migración, CI y Docker.
- Storm-3168: cómo atacaron Azure con service principals comprometidos y cómo proteger tus identidades de workload: un ataque automatizado que borró más de 100 storage accounts en 7 minutos, y un checklist con comandos az.
- Checklist para CTOs: cómo adoptar asistentes de código con IA sin perder calidad ni seguridad: datos, permisos, revisión de código, métricas y formación para adoptar asistentes de código con IA.
Fuentes
- CISA, "Widespread Supply Chain Compromise Impacting npm Ecosystem" (23/09/2025): https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem
- npm Docs, config (
min-release-age,before,allow-scripts,strict-allow-scripts,ignore-scripts): https://docs.npmjs.com/cli/v11/using-npm/config - npm Docs, npm ci: https://docs.npmjs.com/cli/v11/commands/npm-ci
- npm Docs, Verifying ECDSA registry signatures: https://docs.npmjs.com/verifying-registry-signatures
- npm Docs, Trusted publishing for npm packages: https://docs.npmjs.com/trusted-publishers
- npm Docs, Staged publishing: https://docs.npmjs.com/staged-publishing
- npm Docs, Generating provenance statements: https://docs.npmjs.com/generating-provenance-statements
- PyPI Docs, Trusted Publishers: https://docs.pypi.org/trusted-publishers/
- pip, Secure installs (hash-checking mode): https://pip.pypa.io/en/stable/topics/secure-installs/
- uv, Settings (
exclude-newer): https://docs.astral.sh/uv/reference/settings/ - pnpm, Settings (
minimumReleaseAge): https://pnpm.io/settings