Introducción
Los ataques a la cadena de suministro de software han aumentado tanto en frecuencia como en sofisticación en los últimos años. En lugar de atacar las aplicaciones directamente, los adversarios apuntan cada vez más a las capas de resolución de dependencias y distribución de artefactos que sustentan el desarrollo de software moderno. Dos de las técnicas más efectivas en esta categoría son la confusión de dependencias (dependency confusion) y el envenenamiento de artefactos (artifact poisoning).
Estos ataques explotan una verdad fundamental: el software moderno se ensambla, no se escribe desde cero. Una aplicación típica puede incorporar cientos o miles de paquetes de terceros, imágenes de contenedores, templates de CI y plugins de compilación — cada uno representando un eslabón en una cadena de confianza. Cuando cualquier eslabón se compromete, toda la cadena falla.
Lo que hace estos ataques particularmente peligrosos es que muchas organizaciones permanecen vulnerables a pesar de tener programas de seguridad maduros. Las pruebas tradicionales de seguridad de aplicaciones no detectan una dependencia maliciosa instalada en tiempo de compilación. Los firewalls y WAFs son irrelevantes cuando el código del atacante se ejecuta dentro de su pipeline CI/CD con acceso completo a la red y credenciales de producción.
Esta guía proporciona un examen en profundidad de cómo funcionan la confusión de dependencias y el envenenamiento de artefactos, por qué los pipelines CI/CD son objetivos principales y — lo más importante — qué defensas prácticas puede implementar hoy.
Confusión de Dependencias Explicada
Cómo los Package Managers Resuelven Nombres
La mayoría de los package managers modernos — npm, pip, RubyGems, NuGet, Maven — siguen un proceso de resolución que consulta uno o más registros cuando se solicita un paquete. Cuando una organización utiliza tanto un registro privado/interno (para paquetes propietarios) como el registro público (para paquetes open-source), el package manager debe decidir qué registro tiene prioridad cuando ambos contienen un paquete con el mismo nombre.
El comportamiento predeterminado varía según el ecosistema, pero un patrón común es preocupante: muchos package managers preferirán el número de versión más alto independientemente de qué registro provenga. Esta decisión de diseño aparentemente inocente es la base del ataque de confusión de dependencias.
La Investigación Original: Alex Birsan (2021)
En febrero de 2021, el investigador de seguridad Alex Birsan publicó una investigación pionera que demostraba cómo este comportamiento de resolución podía ser utilizado como arma. Al examinar nombres de paquetes internos filtrados públicamente de empresas como Apple, Microsoft y Tesla — encontrados en archivos JavaScript, manifiestos de paquetes y mensajes de error — registró paquetes con nombres idénticos en los registros públicos de npm, PyPI y RubyGems con números de versión inflados.
El resultado fue devastador. Cuando los sistemas de compilación de estas empresas resolvían las dependencias, los package managers obtenían los paquetes públicos de Birsan en lugar de los internos legítimos. Sus paquetes de prueba de concepto incluían callbacks benignos que reportaban a un servidor, confirmando la ejecución de código dentro de las redes corporativas. Birsan ganó más de $130,000 en bug bounties en múltiples organizaciones.
La Mecánica del Ataque
El ataque de confusión de dependencias sigue una secuencia directa:
- Reconocimiento: El atacante identifica nombres de paquetes internos/privados utilizados por la organización objetivo. Estos pueden encontrarse en archivos
package.jsonfiltrados, source maps de JavaScript, mensajes de error, ofertas de empleo o repositorios open-source que referencian dependencias internas. - Registro: El atacante registra un paquete con el nombre idéntico en el registro público correspondiente (npm, PyPI, etc.), asignándole un número de versión muy alto (por ejemplo,
99.0.0). - Payload: El paquete público contiene código malicioso en scripts de instalación o hooks post-instalación — código que se ejecuta automáticamente cuando el paquete se instala.
- Ejecución: Cuando el sistema de compilación de la organización objetivo ejecuta
npm install,pip installo un comando equivalente, el package manager resuelve la dependencia al paquete público del atacante debido al número de versión más alto. - Compromiso: El script de instalación malicioso se ejecuta con los privilegios del proceso de compilación, típicamente obteniendo acceso a variables de entorno (incluyendo secrets), recursos de red y código fuente.
Ecosistemas Afectados
La confusión de dependencias no se limita a un solo lenguaje o ecosistema. Los siguientes son todos susceptibles:
- npm (Node.js): El comportamiento predeterminado puede preferir paquetes públicos sobre privados cuando no se utiliza scoping.
- PyPI (Python): El flag
--extra-index-urlde pip verifica tanto el índice privado como el público, prefiriendo la versión más alta. - RubyGems (Ruby): Comportamiento de resolución similar cuando se configuran múltiples fuentes.
- NuGet (.NET): Verifica múltiples feeds configurados y puede preferir la galería pública.
- Maven (Java): Resuelve desde múltiples repositorios; los atacantes pueden publicar en Maven Central con group/artifact IDs coincidentes.
Install Scripts como Vector de Payload
La razón por la que la confusión de dependencias es tan peligrosa es que los package managers soportan ejecución automática de código durante la instalación. En npm, esto ocurre a través de scripts preinstall, install y postinstall definidos en package.json. En Python, setup.py puede ejecutar código arbitrario durante pip install. Estos hooks fueron diseñados para tareas legítimas de compilación pero proporcionan a los atacantes un vector de ejecución ideal — el código se ejecuta antes de que la aplicación siquiera se compile, a menudo con privilegios elevados.
Envenenamiento de Artefactos: Más Allá de los Paquetes
Mientras que la confusión de dependencias apunta específicamente a los registros de paquetes, el envenenamiento de artefactos es una categoría más amplia de ataques a la cadena de suministro que puede apuntar a cualquier artefacto externo consumido durante el ciclo de vida del desarrollo de software. La superficie de ataque se extiende mucho más allá de los package managers.
Imágenes Base de Contenedores Comprometidas
Las imágenes de contenedores obtenidas de Docker Hub u otros registros públicos son un vector de ataque común. Un atacante puede publicar una imagen maliciosa con un nombre similar a una imagen base popular, o comprometer una imagen existente obteniendo acceso a la cuenta del mantenedor. Si su Dockerfile especifica FROM python:3.11 usando un tag mutable, una imagen comprometida publicada en ese tag será incorporada en cada compilación subsiguiente.
Templates de CI/CD Manipulados
Los templates de GitHub Actions y GitLab CI referenciados desde repositorios públicos representan otra superficie de ataque significativa. Cuando un workflow referencia uses: some-org/some-action@main, el código ejecutado en su pipeline es controlado por quien tenga acceso de push a ese repositorio. Si el repositorio de la action es comprometido, cada pipeline que la referencia también queda comprometido.
Typosquatting
Los ataques de typosquatting explotan errores ortográficos comunes y similitudes visuales en nombres de paquetes. Los ejemplos incluyen registrar co1ors (con un número uno) en lugar de colors, lodahs en lugar de lodash, o reqeusts en lugar de requests. Estos paquetes contienen código malicioso y dependen de que los desarrolladores cometan errores tipográficos al agregar dependencias. Las herramientas automatizadas han detectado miles de paquetes de typosquatting en npm y PyPI.
Cuentas de Mantenedores Secuestradas
Cuando un atacante obtiene acceso a la cuenta de un mantenedor legítimo de paquetes — a través de credential stuffing, phishing o ingeniería social — puede publicar versiones con backdoor de paquetes ampliamente utilizados. Debido a que el nombre del paquete y el mantenedor son legítimos, estas versiones comprometidas son extremadamente difíciles de detectar por medios automatizados.
Plugins de Build Tools
Los sistemas de compilación como Gradle, Maven y webpack soportan plugins que se ejecutan durante el proceso de compilación. Los plugins maliciosos o comprometidos en estos ecosistemas pueden modificar los resultados de compilación, exfiltrar secrets o inyectar backdoors en artefactos compilados. Debido a que los plugins de compilación a menudo reciben menos escrutinio que las dependencias de aplicación, representan un objetivo de alto valor.
Incidentes del Mundo Real
Varios incidentes importantes ilustran el impacto real del envenenamiento de artefactos:
- event-stream (2018): Un nuevo mantenedor recibió derechos de publicación del popular paquete npm
event-stream(1.5 millones de descargas semanales). Agregó una dependencia a un paquete malicioso,flatmap-stream, que contenía código cifrado dirigido a la billetera Bitcoin Copay, intentando robar criptomonedas. - ua-parser-js (2021): El paquete npm
ua-parser-js(7 millones de descargas semanales) fue secuestrado cuando la cuenta del mantenedor fue comprometida. Se publicaron versiones maliciosas que instalaban cryptominers y malware de robo de credenciales en sistemas Linux y Windows. - node-ipc (2022): El mantenedor del paquete
node-ipcañadió deliberadamente código que eliminaba archivos en sistemas con direcciones IP rusas o bielorrusas, demostrando que incluso los mantenedores de confianza pueden convertirse en un vector de amenaza (a veces llamado «protestware»).
Cómo Estos Ataques Explotan CI/CD
Los pipelines CI/CD son particularmente vulnerables a la confusión de dependencias y el envenenamiento de artefactos debido a cómo están diseñados para operar. Comprender por qué los pipelines son objetivos principales es esencial para construir defensas efectivas.
Ejecución Automática de Código Durante las Compilaciones
Cada vez que un pipeline de CI ejecuta npm install, pip install -r requirements.txt o docker build, está ejecutando código de fuentes externas. Esto sucede automáticamente en cada commit, pull request o compilación programada. No hay un humano en el proceso para revisar qué código se está realmente incorporando y ejecutando.
Los Entornos de Compilación Tienen Acceso a Credenciales
Los entornos CI/CD típicamente están configurados con secrets necesarios para el despliegue: credenciales de proveedores cloud, tokens de API, contraseñas de bases de datos, claves de firma y credenciales de registros. Una dependencia maliciosa que se ejecuta durante la fase de compilación puede acceder a estos secrets a través de variables de entorno o almacenes de secrets montados. Esto hace que los pipelines CI/CD sean objetivos mucho más valiosos que las laptops de los desarrolladores.
Las Dependencias Obtenidas en Tiempo de Compilación No Están Pre-Auditadas
En la mayoría de las organizaciones, las versiones de dependencias se especifican en archivos de manifiesto (package.json, requirements.txt) pero el código real se obtiene fresco de los registros en tiempo de compilación. Entre el momento en que un desarrollador agrega una dependencia y cuando el sistema CI la instala, el contenido del paquete podría cambiar — o un paquete de confusión de dependencias podría aparecer en el registro público. Típicamente no hay un paso de verificación entre la resolución y la ejecución.
Las Dependencias Transitivas Expanden la Superficie de Ataque
Su aplicación puede declarar 50 dependencias directas, pero esas dependencias tienen sus propias dependencias, creando un árbol que puede incluir miles de paquetes transitivos. Usted no tiene control directo sobre lo que sus dependencias dependen. Cuando una dependencia transitiva es comprometida — como en el incidente de event-stream — el ataque se propaga a través de todo el árbol de dependencias sin ningún cambio en sus propios archivos de manifiesto.
Defensa Contra la Confusión de Dependencias
Prevenir la confusión de dependencias requiere configurar sus package managers y registros para eliminar la ambigüedad entre paquetes públicos y privados. Aquí están las mitigaciones más efectivas.
Utilice Namespace y Scope en Sus Paquetes Privados
La defensa individual más efectiva es utilizar nombres de paquetes con namespace o scope para todos los paquetes internos. En npm, esto significa usar paquetes con scope como @yourcompany/package-name. Un atacante no puede registrar paquetes bajo el scope de su organización en el registro público de npm.
Configure la Prioridad del Registro Explícitamente
Nunca confíe en el comportamiento de resolución predeterminado. Configure explícitamente su package manager para obtener paquetes con scope de su registro privado y todo lo demás del registro público.
Ejemplo de configuración .npmrc:
# Always fetch @yourcompany scoped packages from private registry
@yourcompany:registry=https://npm.yourcompany.com/
# All other packages come from the public npm registry
registry=https://registry.npmjs.org/
# Authentication for private registry
//npm.yourcompany.com/:_authToken=${NPM_PRIVATE_TOKEN}
Ejemplo de pip.conf para Python:
# IMPORTANT: Use --index-url (NOT --extra-index-url) for your private registry
# --extra-index-url checks BOTH registries and picks the higher version (vulnerable!)
# --index-url uses ONLY your private registry as the primary source
[global]
index-url = https://pypi.yourcompany.com/simple/
# If you need public PyPI packages, configure your private registry
# (Artifactory, Nexus) to proxy public PyPI — do NOT use --extra-index-url
Ejemplo de .yarnrc.yml para Yarn Berry:
npmScopes:
yourcompany:
npmRegistryServer: "https://npm.yourcompany.com"
npmAuthToken: "${NPM_PRIVATE_TOKEN}"
npmRegistryServer: "https://registry.yarnpkg.com"
Utilice Proxies de Registro Privado
Despliegue un proxy de registro como JFrog Artifactory, Sonatype Nexus o GitHub Packages que se sitúe entre sus sistemas de compilación y los registros públicos. Configure el proxy para:
- Servir paquetes internos desde su repositorio privado.
- Hacer proxy de paquetes públicos desde el registro upstream.
- Bloquear cualquier paquete público que comparta nombre con un paquete interno.
- Aplicar políticas de seguridad (escaneo de vulnerabilidades, cumplimiento de licencias) antes de permitir el paso de paquetes.
Esto crea una única fuente de verdad para todas las dependencias y elimina completamente la ambigüedad público/privado.
Registro Defensivo
Registre los nombres de sus paquetes internos en registros públicos como paquetes placeholder. Estos paquetes no deben contener código real — solo un README explicando que el nombre está reservado. Esto evita que los atacantes reclamen esos nombres. Aunque esta no es una defensa primaria, agrega una capa extra de protección.
Fije Dependencias por Hash
Fijar dependencias por hash criptográfico asegura que el artefacto exacto que usted auditó sea el que se instala durante las compilaciones. Incluso si un atacante publica una versión maliciosa, el hash no coincidirá y la instalación fallará.
Para pip (Python):
# Generate hashes for your requirements
pip-compile --generate-hashes requirements.in -o requirements.txt
# Install with hash verification
pip install --require-hashes -r requirements.txt
Para npm:
npm registra automáticamente hashes de integridad en package-lock.json. Asegúrese de hacer commit del lockfile y use npm ci (no npm install) en CI para forzar la verificación de integridad:
# In CI, always use npm ci — it strictly follows the lockfile
# and verifies integrity hashes for every package
npm ci
Defensa Contra el Envenenamiento de Artefactos
Debido a que el envenenamiento de artefactos abarca una gama más amplia de vectores de ataque, la defensa requiere controles en capas a través de imágenes de contenedores, templates de CI, dependencias y procesos de compilación.
Fije Imágenes de Contenedores por Digest
Nunca referencie imágenes de contenedores por tags mutables como latest o incluso 3.11. En su lugar, fije al digest SHA256 inmutable:
# Vulnerable: tag can be overwritten with a compromised image
FROM python:3.11-slim
# Secure: digest is immutable — this exact image or nothing
FROM python:3.11-slim@sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Fije GitHub Actions por SHA
Referencie GitHub Actions por su SHA de commit completo en lugar de un tag mutable:
# Vulnerable: v3 tag can be moved to point to compromised code
- uses: actions/checkout@v3
# Secure: pinned to specific commit SHA
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
Verifique Firmas en las Dependencias
Donde esté disponible, verifique firmas criptográficas para confirmar que los artefactos fueron publicados por sus mantenedores esperados:
- npm: Use
npm audit signaturespara verificar firmas del registro en los paquetes. - Imágenes de contenedores: Use Sigstore/cosign para verificar firmas de imágenes de contenedores.
- Python: PEP 740 y herramientas como
sigstore-pythonestán trayendo la verificación de firmas a PyPI.
Genere y Rastree SBOMs
Un Software Bill of Materials (SBOM) inventaría cada componente en su aplicación. Al generar SBOMs para cada compilación y compararlos, puede detectar adiciones o cambios inesperados en su árbol de dependencias. Herramientas como Syft, Trivy y CycloneDX pueden generar SBOMs en formatos estándar (SPDX, CycloneDX).
Automatice la Revisión de Dependencias
Despliegue herramientas automatizadas que escaneen continuamente sus dependencias en busca de vulnerabilidades conocidas y cambios sospechosos:
- Dependabot / Renovate: Crean automáticamente PRs cuando hay actualizaciones de dependencias disponibles, dándole la oportunidad de revisar antes de hacer merge.
- npm audit / pip-audit: Escanean vulnerabilidades conocidas en su árbol de dependencias.
- GitHub Dependency Review Action: Bloquea PRs que introducen dependencias con vulnerabilidades conocidas.
Restrinja el Acceso a Red Durante las Compilaciones (Hermetic Builds)
Los hermetic builds son compilaciones que no pueden acceder a la red. Todas las dependencias deben ser pre-obtenidas y cacheadas antes de que comience la compilación. Esto evita que una compilación obtenga un paquete malicioso recién publicado. Bazel soporta nativamente hermetic builds, y un aislamiento similar puede lograrse con el flag --network=none de Docker o políticas de red específicas de la plataforma CI.
Pre-Apruebe y Cree Allowlists de Dependencias
Mantenga una lista aprobada de dependencias y sus versiones. Cualquier nueva dependencia o cambio de versión requiere aprobación explícita a través de un proceso de revisión. Aunque esto agrega fricción, previene que paquetes no autorizados entren en su pipeline de compilación.
Detección y Monitoreo
Incluso con controles preventivos fuertes, las capacidades de detección son esenciales para capturar ataques que eludan sus defensas.
Monitoree Nuevas Dependencias Inesperadas
Implemente verificaciones de CI que marquen pull requests que introduzcan nuevas dependencias. Requiera justificación y revisión explícita para cualquier adición a su manifiesto de dependencias. Esto es particularmente importante para dependencias transitivas — una nueva dependencia directa podría traer docenas de transitivas.
Alerte sobre Saltos de Versión de Dependencias
Una dependencia que salta de la versión 1.2.3 a 99.0.0 es un fuerte indicador de un ataque de confusión de dependencias. Implemente monitoreo que alerte sobre cambios de versión inusuales, especialmente grandes saltos de versión mayor en paquetes internos.
Aproveche las Herramientas de Escaneo de Seguridad
- Socket.dev: Analiza el comportamiento de los paquetes (acceso a red, acceso al sistema de archivos, install scripts) en lugar de solo CVEs conocidos, haciéndolo efectivo para detectar ataques a la cadena de suministro.
- Snyk: Proporciona escaneo y monitoreo de vulnerabilidades en múltiples ecosistemas.
- GitHub Dependency Graph y Dependabot Alerts: Rastrean automáticamente las dependencias y alertan sobre vulnerabilidades conocidas.
Escanee Install Scripts y Hooks Post-Instalación
Implemente herramientas que identifiquen específicamente paquetes con install scripts. Aunque los install scripts tienen usos legítimos, son el vector de ejecución principal para ataques de confusión de dependencias. Marque y revise cualquier paquete que incluya scripts preinstall, install o postinstall en npm, o un setup.py con código ejecutable en Python.
Compare SBOMs Entre Compilaciones
Compare regularmente SBOMs de compilaciones consecutivas. Cambios inesperados — nuevos paquetes que aparecen, versiones que cambian sin actualizaciones correspondientes del manifiesto, o paquetes de registros inesperados — deberían activar alertas e investigación.
Hardening Específico de CI/CD
Más allá de la gestión de dependencias, la configuración de su pipeline CI/CD en sí necesita hardening para resistir ataques a la cadena de suministro.
Haga Commit y Verifique Lock Files en CI
Los lock files (package-lock.json, yarn.lock, Pipfile.lock, poetry.lock) registran las versiones exactas y hashes de cada dependencia. Su pipeline de CI debería fallar si el lock file tiene cambios inesperados.
Ejemplo de GitHub Actions — verificar integridad del lockfile:
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Setup Node.js
uses: actions/setup-node@1a4442cacd436585916f1b3aa94e4166f1a22160 # v3.8.2
with:
node-version: '20'
- name: Verify lockfile has not been tampered with
run: |
# npm ci will fail if package-lock.json is out of sync
# with package.json or if integrity hashes don't match
npm ci
- name: Check for lockfile modifications
run: |
if ! git diff --exit-code package-lock.json; then
echo "ERROR: package-lock.json was modified during install."
echo "This could indicate a dependency confusion attack."
exit 1
fi
- name: Audit dependencies
run: npm audit --audit-level=high
Ejemplo de GitLab CI — verificación de lockfile con pip:
stages:
- verify
- build
- test
verify-dependencies:
stage: verify
image: python:3.11-slim@sha256:abc123... # Pin by digest
script:
- pip install pip-tools pip-audit
# Verify that requirements.txt hashes match actual packages
- pip install --require-hashes --no-deps -r requirements.txt
# Audit for known vulnerabilities
- pip-audit -r requirements.txt
# Ensure no unexpected changes to lockfile
- pip-compile --generate-hashes requirements.in -o /tmp/requirements-check.txt
- diff requirements.txt /tmp/requirements-check.txt
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
build:
stage: build
image: python:3.11-slim@sha256:abc123...
script:
- pip install --require-hashes --no-deps -r requirements.txt
- python -m build
needs: [verify-dependencies]
Separe la Resolución de Dependencias de la Ejecución de Compilación
Use un proceso de compilación en dos fases: primero resuelva y descargue las dependencias en un entorno aislado, luego ejecute la compilación real con el acceso a red deshabilitado. Esto evita que una dependencia comprometida exfiltre datos o descargue payloads adicionales durante la compilación.
Entornos de Compilación Air-Gapped o con Red Restringida
Para compilaciones de alta seguridad, use entornos con red restringida que solo puedan acceder a su proxy de registro interno. Esto elimina la posibilidad de que las dependencias se obtengan directamente de registros públicos durante la compilación.
# Docker-based hermetic build example
# Phase 1: Fetch dependencies (with network)
docker run --name dep-fetch my-builder:latest \
npm ci --prefer-offline
# Phase 2: Build (without network)
docker run --network=none -v deps:/app/node_modules \
my-builder:latest npm run build
Cachee Dependencias en Almacenamiento Interno de Confianza
En lugar de obtener dependencias de registros públicos en cada compilación, cachee versiones aprobadas en almacenamiento interno (Artifactory, Nexus, buckets de almacenamiento cloud). Su pipeline de CI debería obtener exclusivamente de este caché de confianza. Actualice el caché a través de un proceso controlado y auditado.
Conclusión
La confusión de dependencias y el envenenamiento de artefactos explotan suposiciones de confianza profundamente arraigadas en la cadena de suministro de software. Cada npm install, cada docker pull, cada directiva uses: en un workflow de GitHub Actions es una decisión de confianza — y los atacantes están trabajando activamente para abusar de esa confianza.
La defensa efectiva no es una sola herramienta o cambio de configuración. Requiere controles en cada capa del ciclo de vida de las dependencias:
- Resolución: Aplique scope a sus paquetes, configure la prioridad del registro, use proxies de registro para eliminar la ambigüedad público/privado.
- Instalación: Fije por hash, use lockfiles con
npm cio--require-hashes, deshabilite install scripts donde sea posible. - Verificación: Verifique firmas, compare SBOMs, audite dependencias antes de que entren en su compilación.
- Monitoreo: Detecte cambios inesperados de dependencias, alerte sobre anomalías de versión, escanee comportamiento malicioso en paquetes.
- Hardening del pipeline: Restrinja el acceso a red durante las compilaciones, verifique la integridad del lockfile en CI, separe la resolución de dependencias de la ejecución de compilación.
Las organizaciones que serán más resilientes a los ataques a la cadena de suministro son aquellas que tratan las dependencias con el mismo rigor que aplican a su propio código: revisadas, verificadas, monitoreadas y nunca confiadas implícitamente. Comience implementando los controles de mayor impacto — paquetes con scope, configuración de registro, verificación de lockfile en CI — y agregue capas progresivamente a medida que su programa de seguridad de cadena de suministro madure.