Ataques de confusión de dependencias y cómo prevenirlos
Cómo los ataques de confusión de dependencias explotan los gestores de paquetes para inyectar código malicioso, y las defensas concretas que lo evitan.

En febrero de 2021, un investigador de seguridad demostró que podía ejecutar código dentro de las redes de Apple, Microsoft, PayPal y decenas de otras empresas aprovechando una vulnerabilidad en la forma en que los gestores de paquetes resuelven las dependencias. A esta técnica se le llama confusión de dependencias, y funciona porque la mayoría de gestores de paquetes consultan los registros públicos antes —o junto— a los privados.
Si tu organización publica paquetes internos en un registro privado, eres vulnerable a menos que hayas tomado medidas explícitas para evitarlo.
Cómo funciona el ataque
El ataque se aprovecha de la brecha entre los registros de paquetes privados y públicos. Cuando un proyecto depende de @company/auth-utils desde un registro privado, el gestor de paquetes también podría consultar el registro público de npm. Si un atacante publica un paquete llamado auth-utils (sin el scope) en npm con un número de versión superior, algunas configuraciones preferirán la versión pública.
// ❌ Vulnerable package.json — unscoped private package names
{
"dependencies": {
"auth-utils": "^1.2.0",
"payment-service-sdk": "^3.0.0",
"internal-logger": "^2.1.0"
}
}
// An attacker publishes "auth-utils@99.0.0" to npm
// Package manager sees the higher version on the public registry
// Installs the attacker's malicious package instead// ✅ Scoped packages with registry pinning
{
"dependencies": {
"@yourcompany/auth-utils": "^1.2.0",
"@yourcompany/payment-service-sdk": "^3.0.0",
"@yourcompany/internal-logger": "^2.1.0"
}
}
// Scoped packages (@yourcompany/*) route to your private registry
// Attacker cannot claim your organization's scope on npmEl ataque depende de tres condiciones: nombres de paquetes internos sin scope, un gestor de paquetes que consulta ambos registros, y la ausencia de versiones fijas o comprobaciones de integridad. Elimina cualquiera de ellas y el ataque fracasa.
Capa de defensa 1: Asignar scope a todos los paquetes internos
La defensa más sencilla y efectiva es usar scopes de npm para todos los paquetes internos. Cuando eres dueño del scope @yourcompany en el registro público de npm, nadie más puede publicar paquetes bajo ese scope.
# Claim your organization's scope on npm (even if you never publish there)
npm login --registry=https://registry.npmjs.org
npm org create yourcompany
# Now no attacker can publish @yourcompany/* packages publicly// ❌ Internal package without scope
// package.json of your internal library
{
"name": "feature-flags-sdk",
"version": "1.3.0"
}
// Attacker can publish "feature-flags-sdk@99.0.0" on npm
// ✅ Internal package with scope
{
"name": "@yourcompany/feature-flags-sdk",
"version": "1.3.0"
}
// Only members of @yourcompany npm org can publish thisIncluso si nunca publicas paquetes en el registro público, reclama tu scope de organización de forma defensiva. Es gratis y bloquea el vector de ataque más común.
Capa de defensa 2: Configuración del registro
Configura tu gestor de paquetes para que dirija los scopes internos a tu registro privado y todo lo demás al registro público. Nunca dejes que el gestor de paquetes "busque" en ambos registros el mismo paquete.
# .npmrc — explicit registry routing
# All @yourcompany packages come from your private registry
@yourcompany:registry=https://npm.yourcompany.com/
# Everything else comes from the public registry
registry=https://registry.npmjs.org/
# Optional: require authentication for private registry
//npm.yourcompany.com/:_authToken=${NPM_PRIVATE_TOKEN}# .yarnrc.yml (Yarn Berry / Yarn 2+)
npmScopes:
yourcompany:
npmRegistryServer: "https://npm.yourcompany.com/"
npmAuthToken: "${NPM_PRIVATE_TOKEN}"
npmRegistryServer: "https://registry.npmjs.org/"// ❌ Single registry fallback — dangerous
// .npmrc with only:
// registry=https://npm.yourcompany.com/
// If private registry is down, npm falls back to public registry
// Attacker waits for an outage, or the fallback fetches malicious packages
// ✅ Explicit routing — no fallback ambiguity
// .npmrc with scoped registries as shown above
// @yourcompany/* → private registry (no fallback)
// everything else → public npm (no confusion)Capa de defensa 3: Archivos de bloqueo y comprobaciones de integridad
Los archivos de bloqueo fijan versiones exactas e incluyen hashes de integridad. Incluso si el gestor de paquetes resuelve una versión maliciosa, la discrepancia del hash de integridad hace que la instalación falle.
// package-lock.json includes integrity hashes
{
"node_modules/@yourcompany/auth-utils": {
"version": "1.2.0",
"resolved": "https://npm.yourcompany.com/@yourcompany/auth-utils/-/auth-utils-1.2.0.tgz",
"integrity": "sha512-abc123def456..."
}
}# CI should always use frozen lockfile installation
# npm
npm ci # Fails if lock file is out of sync with package.json
# yarn
yarn install --frozen-lockfile
# pnpm
pnpm install --frozen-lockfile# ❌ Running 'npm install' in CI
# This can update the lock file, potentially pulling in new versions
npm install
# ✅ Running 'npm ci' in CI
# This installs exactly what the lock file specifies
# Fails fast if lock file and package.json disagree
npm ciConfirma tu archivo de bloqueo en el control de versiones y exige npm ci (o equivalente) en todos los pipelines de CI. Cualquier desarrollador que ejecute npm install y obtenga cambios inesperados en el archivo de bloqueo debe investigar antes de confirmarlos.
Capa de defensa 4: Auditoría automatizada
Ejecuta auditorías de dependencias como parte de tu pipeline de CI. Busca vulnerabilidades conocidas y fuentes de paquetes inesperadas.
# GitHub Actions — dependency audit step
name: Security Audit
on:
pull_request:
paths:
- 'package.json'
- 'package-lock.json'
- '.npmrc'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install with frozen lockfile
run: npm ci
- name: Run npm audit
run: npm audit --audit-level=high
- name: Check package sources
run: |
# Verify all @yourcompany packages resolve to private registry
grep -E '"resolved": "https://npm.yourcompany.com' package-lock.json \
| wc -l
# Verify NO @yourcompany packages resolve to public registry
if grep -E '@yourcompany.*registry.npmjs.org' package-lock.json; then
echo "ERROR: Internal package resolving to public registry!"
exit 1
fi// Custom script to validate package sources
import { readFileSync } from 'fs';
interface PackageLockEntry {
version: string;
resolved: string;
integrity: string;
}
function auditPackageSources(lockfilePath: string): void {
const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf-8'));
const packages = lockfile.packages || {};
for (const [name, meta] of Object.entries<PackageLockEntry>(packages)) {
if (name.includes('@yourcompany/')) {
const isPrivateRegistry = meta.resolved?.startsWith(
'https://npm.yourcompany.com'
);
if (!isPrivateRegistry) {
console.error(
`SECURITY: ${name} resolves to unexpected registry: ${meta.resolved}`
);
process.exit(1);
}
}
}
console.log('All internal packages resolve to private registry.');
}
auditPackageSources('package-lock.json');Resumen de la defensa en profundidad
Ninguna defensa por sí sola es suficiente. Apílalas:
defense_layers:
1_scoping:
action: "Scope all internal packages under @yourcompany"
blocks: "Attacker cannot claim your scope on public registry"
2_registry_routing:
action: "Pin scopes to specific registries in .npmrc"
blocks: "Package manager never queries wrong registry"
3_lock_files:
action: "Use npm ci with integrity hashes in CI"
blocks: "Even if resolution is wrong, hash mismatch stops install"
4_auditing:
action: "Automated CI checks for package source URLs"
blocks: "Catches misconfigurations before they reach production"
5_monitoring:
action: "Alert on new packages appearing in lock file diffs"
blocks: "Human review of unexpected dependency changes"Conclusiones clave
- La confusión de dependencias explota el orden de resolución del gestor de paquetes — los paquetes públicos con versiones superiores anulan a los privados
- Asigna scope a todos los paquetes internos (
@yourcompany/*) y reclama el scope de la organización en los registros públicos de forma defensiva - Fija los paquetes con scope a tu registro privado en
.npmrc— nunca dejes que los gestores de paquetes busquen el mismo nombre en ambos registros - Usa
npm cien los pipelines de CI — los archivos de bloqueo congelados con hashes de integridad detectan discrepancias de resolución - Automatiza la auditoría de fuentes — verifica que los paquetes internos siempre se resuelvan en tu registro privado, no en npm
- Capas tus defensas — el scope, la configuración del registro, los archivos de bloqueo y la auditoría juntos proporcionan una protección robusta


