Ataque de confusión de dependencias: defensas para npm y PyPI
Un ataque de confusión de dependencias (dependency confusion) aprovecha cómo resuelven las dependencias los gestores de paquetes: un atacante publica un paquete malicioso en un registro público (como npm o PyPI) con el mismo nombre que uno de tus paquetes internos y privados. Cuando tu herramienta de compilación ve dos paquetes con el mismo nombre, a menudo elige por defecto el de número de versión más alto (que el atacante fija artificialmente alto) y mete su código directamente en tu compilación. Es uno de los patrones de ataque a la cadena de suministro de paquetes más conocidos, junto con el typosquatting y los scripts de instalación que exfiltran credenciales.
Qué es realmente una dependencia y por qué se produce la confusión
Una dependencia es cualquier código externo del que depende tu aplicación para funcionar. Cuando escribes import requests en Python o require('lodash') en Node, estás declarando una dependencia que tu gestor de paquetes debe descargar e instalar. Las aplicaciones modernas tienen cientos, la mayoría descargadas automáticamente de registros públicos.
La confusión surge de una colisión de espacios de nombres. Las grandes organizaciones crean paquetes internos (por ejemplo, acme-internal-auth) alojados en un registro privado. Si el gestor de paquetes está configurado para consultar tanto el registro privado como el público, y no existe ningún paquete público con ese nombre, un atacante puede registrar acme-internal-auth en público. Como la copia pública es la versión 99.0.0 y tu copia interna es la 1.4.2, el resolvedor se queda con la versión del atacante. Nadie escribió nada mal ni sufrió phishing: el sistema funcionó exactamente como estaba diseñado, en tu contra.
Un ejemplo real de ataque de confusión de dependencias
La técnica la demostró a escala el investigador de seguridad Alex Birsan, que publicó su investigación en febrero de 2021. Recopiló nombres de paquetes internos de archivos package.json expuestos en JavaScript y repositorios públicos, y después publicó paquetes con esos nombres en npm, PyPI y RubyGems con un código inofensivo que «llamaba a casa». Esos paquetes se ejecutaron dentro de las redes de más de 35 empresas, entre ellas Apple, Microsoft y PayPal, le reportaron más de 130.000 dólares en recompensas por fallos y convirtieron de la noche a la mañana la «confusión de dependencias» en una clase de ataque reconocida.
Una variante de confusión de dependencias en Python sigue la misma lógica con pip. Si instalas paquetes internos con --extra-index-url apuntando a la vez a un índice privado y a PyPI, pip tendrá en cuenta ambas fuentes y seleccionará la versión más alta, venga del índice que venga. Un atacante que conozca el nombre de un módulo interno (por ejemplo, corp-data-utils) puede publicarlo en el PyPI público y hacer que sustituya en silencio a tu compilación privada. La solución en el mundo de pip es evitar --extra-index-url y usar un índice interno curado que haga de proxy de los paquetes públicos, o fijar versiones y hashes exactos.
Dónde encaja esto en el panorama de los ataques a la cadena de suministro
La confusión de dependencias es una variante de ataque a la cadena de suministro de software. Otras que deberías conocer:
- Typosquatting: registrar
reqeustsoloadashpara atrapar a los desarrolladores que teclean mal el nombre de un paquete. - Abuso de scripts post-instalación: código malicioso en el hook
postinstallde npm que se ejecuta automáticamente connpm instally exfiltra credenciales, claves SSH o variables de entorno. - Cuentas de mantenedores comprometidas: los atacantes secuestran un paquete popular legítimo y publican una actualización maliciosa (como ocurrió con event-stream y, más recientemente, con varias campañas de robo de criptomonedas en npm).
- Compromiso del sistema de compilación: el patrón de SolarWinds, en el que el atacante envenena el artefacto durante la compilación y no en una dependencia.
Lo que los une es la confianza implícita: tu compilación ejecuta automáticamente código de terceros que nunca revisaste. Por eso la Orden Ejecutiva 14028 (mayo de 2021) puso los SBOM en el mapa: ordenó al NIST publicar directrices de software seguro (incluida la entrega de un SBOM a los compradores) y dio lugar al memorando M-22-18 de la OMB, que exige a los proveedores que venden software a las agencias federales de EE. UU. certificar sus prácticas de desarrollo seguro (las agencias también pueden pedir un SBOM). Rige el software que se vende a la administración federal, no a las empresas privadas en general, pero la lección es trasladable: no puedes defender una cadena de suministro que no puedes enumerar.
Cómo evitar la confusión de dependencias: un checklist concreto
Prioriza estos controles en este orden:
- Reclama tus nombres en público. Registra el scope de tu organización en npm (por ejemplo, la organización
@acme) para que nadie más pueda publicar con él y, donde la política del registro lo permita, reserva en PyPI los nombres internos sin scope. Las políticas de npm desaconsejan los paquetes de relleno para acaparar nombres, otra razón para trasladar el código interno de npm a un scope. - Usa paquetes con scope. En npm, publica el código interno bajo un scope como
@acme/y asocia ese scope a tu feed privado en.npmrc(@acme:registry=https://npm.internal.example/), para que nunca se resuelva desde el registro público. Un nombre con scope no lo puede acaparar alguien que no controle el scope. - Fija las versiones y verifica la integridad. Confirma en el repositorio los lockfiles (
package-lock.json,poetry.lock) con hashes de integridad para que un paquete sustituido no supere la verificación. - Configura un único registro curado. Apunta las compilaciones a un único proxy interno (Artifactory, Nexus, Verdaccio o devpi para Python) que replique los paquetes públicos aprobados, en lugar de mezclar índices privados y públicos en el momento de la resolución. Para pip, eso significa
--index-urlapuntando al proxy y ningún--extra-index-url. Comprueba cómo resuelve el propio proxy un nombre que existe tanto en sus repositorios locales como en los remotos: JFrog Artifactory tiene una opción de resolución prioritaria para los repositorios locales, y Repository Firewall de Sonatype ofrece protección frente a la confusión de espacios de nombres. - Genera un SBOM. Produce un SBOM en CycloneDX o SPDX por cada compilación para saber exactamente qué se publicó; cada vez se pide más en las compras federales de EE. UU. y es útil en todas partes.
Para revisar los paquetes públicos que descargan tus compilaciones, el NPM Package Scanner de PlayCISO puntúa los paquetes npm con más de 40 políticas de riesgo, entre ellas scripts de instalación maliciosos, transferencias de propiedad y typosquats. No puede ver tu registro privado, así que los pasos de scope y de registro de arriba siguen siendo cosa tuya.
Preguntas frecuentes
¿Qué es un ataque de confusión de dependencias?
Un atacante publica un paquete en un registro público como npm o PyPI con el mismo nombre que uno de tus paquetes internos y un número de versión más alto. Si tu gestor de paquetes consulta tanto tu registro privado como el público, puede elegir la versión más alta del atacante y ejecutar su código en tu compilación.
¿Cómo se evita la confusión de dependencias en npm?
Pon los paquetes internos bajo un scope que controles (por ejemplo, @acme), registra ese scope en el registro público de npm, asocia el scope a tu registro privado en .npmrc, confirma en el repositorio los lockfiles con hashes de integridad y haz pasar todas las instalaciones por un único proxy curado.
¿Cómo se evita la confusión de dependencias en pip?
No uses --extra-index-url para combinar un índice privado con PyPI, porque pip elige la versión más alta de cualquiera de los dos. Apunta --index-url a un único proxy curado que sirva tanto tus paquetes internos como los paquetes de PyPI aprobados, y fija las versiones con hashes.