El incidente que le dio origen
En 2026 una dependencia de npm fue secuestrada por una toma de cuenta del mantenedor. El atacante metió el payload dentro de un archivo de configuración legítimo (postcss.config.mjs), empujado a la derecha detrás de cerca de 10 mil espacios en la línea del export default. En el editor se veía un archivo de config normal. En el git diff no aparecía nada raro. Pero al correr el build, ese código se ejecutaba con todos los privilegios de Node del usuario.
Una vez deofuscado, el payload era un loader de RAT con un mecanismo de comando y control poco común: un blockchain dead-drop. En lugar de hablar con un servidor que se pueda tumbar, leía la última transacción de una wallet de TRON, la convertía en un hash de transacción de BSC, descargaba el input de esa transacción, lo decodificaba a un comando y lo ejecutaba con node -e. Ejecución remota completa, a demanda, por un canal descentralizado que no se puede dar de baja.
El punto de entrada fue una dependencia sin fijar. Eso es lo que pin-guard elimina.
Qué atrapa el escaneo
- Specs sin fijar (acento circunflejo, virgulilla, asterisco,
latest, versiones faltantes) enpackage.json, scripts de npm, configs de MCP (.mcp.json,.claude/settings.json,.cursor/mcp.json) y GitHub Actions. - Marcadores del inyector en código JavaScript y TypeScript.
- Archivos de config inflados con una corrida de 200+ caracteres de espacios antes del código. El tamaño solo es una advertencia; la corrida de espacios es la firma real de la inyección.
- Versiones comprometidas conocidas en lockfiles de npm, pnpm, yarn y bun.
- Referencias a hosts o wallets de comando y control en el código fuente (la firma del loader del RAT).
- Droppers tipo
setup.cjsy entradas.baten.gitignore. - Lockfile faltante,
npm installen lugar denpm ci, y.npmrcsinsave-exact.
Defensa en capas
| Capa | Herramienta | Qué atrapa |
|---|---|---|
| Pre-vuelo | Regla en CLAUDE.md | Que el agente escanee antes de tocar el repo |
| Pre-commit | install-hook.sh | git bloquea commits infectados |
| Pre-install | verify.py | el paquete malo antes de que aterrice |
| Estática | scan.py | marcadores, configs, lockfiles, refs de C2 |
| Pinning | pin.py | quita el riesgo de versiones sin fijar |
Lo que probé antes de curarlo
No publico nada que no haya corrido. Cloné el repo, leí los cinco scripts línea por línea y los probé:
scan.pysobre un repo real de Next.js: cero falsos positivos de malware, solo reportó las dependencias con rango sin fijar (que es su trabajo).verify.py axios@1.12.3(una versión comprometida conocida): bloqueó correctamente con código de salida 2.verify.pycon un nombre de paquete malicioso disfrazado de bandera de npm: lo rechazó antes de consultar el registro. La protección contra argument-injection funciona.
El código es honesto: el escaneo es de solo lectura, el pin es dry-run por default y no tiene una sola dependencia externa.
Limitaciones honestas
- Es un proyecto nuevo (junio de 2026), de un solo mantenedor y con pocas estrellas. El valor está en lo simple y auditable del código, no en una comunidad grande detrás.
- La capa de runtime que recomienda en su documentación instala un plugin de terceros desde un marketplace externo. Esa parte la dejaría fuera hasta auditarla por separado: agregar supply-chain externa sin revisar contradice el resto de la herramienta.
- Es defensa de supply-chain, no un antivirus. Si tu máquina ya está comprometida, pin-guard no la limpia; te dice que no sigas y que rotes credenciales primero.
Mi recomendación
Si tu equipo adoptó Claude Code o Cursor y esos agentes corren npx contra el registro de npm, tienes esta superficie de ataque abierta hoy. Correr scan.py no cuesta nada y te dice en minutos qué tan expuesto estás. Fijar versiones y meter el hook de pre-commit es trabajo de una tarde que cierra la puerta por la que entró el incidente de 2026. El downside es nulo (Python puro, sin dependencias, el escaneo no modifica nada) y el upside es no ser la próxima nota de un RAT escondido detrás de 10 mil espacios.