Seguridad de la cadena de suministro: ataques a dependencias
Guía completa para proteger tu cadena de suministro frente a la confusión de dependencias, el typosquatting, los paquetes comprometidos y el pipeline.

La superficie de ataque que no puedes ver
Tu aplicación es un 10% código propio y un 90% código de otras personas. Un proyecto típico de Node.js incorpora cientos de dependencias transitivas. Cada una de ellas es una decisión de confianza: confías en que la cuenta de npm del mantenedor no esté comprometida, en que el registro de paquetes no haya sido manipulado y en que cada dependencia transitiva, tres niveles más abajo, sea igual de fiable.
El ataque a SolarWinds, el compromiso de ua-parser-js, la puerta trasera de event-stream, el sabotaje de colors.js: no son riesgos teóricos. Son incidentes documentados en los que paquetes de confianza se convirtieron en vectores de ataque. La seguridad de la cadena de suministro ya no es opcional; es una responsabilidad fundamental de la ingeniería.
Dependency Confusion: la trampa de los paquetes internos
Dependency Confusion aprovecha la forma en que los gestores de paquetes resuelven los nombres. Si tu empresa usa un paquete privado llamado @company/auth-utils, un atacante publica un paquete con el mismo nombre en el registro público de npm, pero con un número de versión más alto. Si tu gestor de paquetes consulta primero el registro público, instalará la versión del atacante.
// ❌ Bad: No registry scoping — vulnerable to dependency confusion
{
"dependencies": {
"auth-utils": "^2.0.0",
"payment-sdk": "^1.5.0"
}
}// ✅ Good: Scoped packages with registry pinning
{
"dependencies": {
"@mycompany/auth-utils": "^2.0.0",
"@mycompany/payment-sdk": "^1.5.0"
}
}# .npmrc — Pin scoped packages to your private registry
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_PRIVATE_TOKEN}Los paquetes con alcance (scoped) y un anclaje explícito de registro en .npmrc evitan que el gestor de paquetes llegue a consultar el registro público para tus paquetes internos. Esta es la defensa más simple y eficaz contra los ataques de Dependency Confusion.
Integridad del lockfile: tu primera línea de defensa
El lockfile fija versiones exactas de las dependencias e incluye hashes de integridad. Si alguien modifica una dependencia entre instalaciones, ya sea por un registro comprometido o por un ataque a la cadena de suministro, la verificación de integridad falla.
// Script to verify lockfile integrity in CI
import { readFileSync } from "fs";
import crypto from "crypto";
interface LockfilePackage {
version: string;
resolved: string;
integrity: string;
}
function verifyLockfileIntegrity(lockfilePath: string): {
valid: boolean;
issues: string[];
} {
const issues: string[] = [];
const lockfile = JSON.parse(readFileSync(lockfilePath, "utf-8"));
const packages: Record<string, LockfilePackage> =
lockfile.packages || {};
for (const [name, pkg] of Object.entries(packages)) {
if (!name) continue; // Skip root
// Check for missing integrity hashes
if (!pkg.integrity) {
issues.push(`Missing integrity hash: ${name}@${pkg.version}`);
}
// Check for non-registry URLs
if (
pkg.resolved &&
!pkg.resolved.startsWith("https://registry.npmjs.org/") &&
!pkg.resolved.startsWith("https://npm.mycompany.com/")
) {
issues.push(`Unexpected registry URL: ${name} → ${pkg.resolved}`);
}
// Check for git dependencies (potential risk)
if (pkg.resolved?.startsWith("git+")) {
issues.push(`Git dependency detected: ${name} → ${pkg.resolved}`);
}
}
return { valid: issues.length === 0, issues };
}# CI pipeline step: Verify lockfile before install
- name: Verify dependency integrity
run: |
# Fail if lockfile would change (ensures it's committed and current)
npm ci --ignore-scripts
# The --ignore-scripts flag prevents pre/post install scripts from running
# during the verification phaseUsa siempre npm ci (o bun install --frozen-lockfile) en CI, nunca npm install. El comando ci falla si el lockfile no coincide con package.json, lo que evita una desviación silenciosa de las dependencias.
Detección de typosquatting y paquetes maliciosos
Los paquetes de typosquatting tienen nombres que se parecen a los legítimos: lodahs en lugar de lodash, cross-env2 en lugar de cross-env. El escaneo automatizado los detecta antes de que entren en tu árbol de dependencias.
// Pre-install hook to check for suspicious packages
import { execSync } from "child_process";
interface PackageAudit {
name: string;
version: string;
riskLevel: "low" | "medium" | "high" | "critical";
reasons: string[];
}
function auditNewDependency(packageName: string): PackageAudit {
const reasons: string[] = [];
let riskLevel: "low" | "medium" | "high" | "critical" = "low";
// Check package age
const info = JSON.parse(
execSync(`npm view ${packageName} --json`, {
encoding: "utf-8",
})
);
const createdDate = new Date(info.time?.created);
const ageInDays =
(Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24);
if (ageInDays < 30) {
reasons.push(`Package is only ${Math.floor(ageInDays)} days old`);
riskLevel = "high";
}
// Check download count
const downloads = JSON.parse(
execSync(
`npm view ${packageName} --json | jq '.downloads'`,
{ encoding: "utf-8" }
).trim() || "0"
);
if (typeof downloads === "number" && downloads < 100) {
reasons.push(`Low download count: ${downloads}`);
riskLevel = riskLevel === "high" ? "critical" : "high";
}
// Check for install scripts
if (info.scripts?.preinstall || info.scripts?.postinstall) {
reasons.push("Contains install scripts");
riskLevel = "medium";
}
// Check maintainer count
const maintainers = info.maintainers || [];
if (maintainers.length === 1) {
reasons.push("Single maintainer");
}
return {
name: packageName,
version: info["dist-tags"]?.latest,
riskLevel,
reasons,
};
}Las señales de alerta: paquetes nuevos con scripts de instalación, un único mantenedor, pocas descargas y nombres similares a los de paquetes populares. Ninguna señal por sí sola es concluyente, pero la combinación dibuja un panorama claro.
Anclaje y automatización de actualizaciones
Los rangos de versión (^ y ~) permiten actualizaciones automáticas de tipo menor y de parche, que es precisamente el mecanismo por el que se propagan las versiones comprometidas. Anclar versiones exactas te da control sobre cuándo se producen las actualizaciones.
// ❌ Bad: Version ranges allow silent updates
{
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.0"
}
}// ✅ Good: Exact versions with automated update PRs
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21"
}
}# Renovate config for controlled dependency updates
# renovate.json
{
"extends": ["config:base"],
"rangeStrategy": "pin",
"schedule": ["after 10pm every weekday", "before 5am every weekday"],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"schedule": ["at any time"]
},
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"automergeType": "pr",
"requiredStatusChecks": ["ci"]
},
{
"matchDepTypes": ["dependencies"],
"automerge": false,
"reviewers": ["team:security"]
}
]
}Ancla versiones exactas y luego usa Renovate o Dependabot para crear pull requests con las actualizaciones. Las dependencias de desarrollo pueden fusionarse automáticamente una vez que el CI pasa. Las dependencias de producción requieren revisión humana. Las vulnerabilidades de seguridad generan PRs inmediatos, sin importar el calendario.
Refuerzo del pipeline de compilación
El pipeline de compilación es otra superficie de ataque. Si un atacante compromete tu entorno de CI, puede inyectar código durante el proceso de build, incluso si tu código fuente está limpio.
# GitHub Actions with security hardening
name: Build and Deploy
on:
push:
branches: [main]
permissions:
contents: read # Minimum required permissions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # Don't leak tokens
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".node-version"
- name: Install dependencies
run: npm ci --ignore-scripts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Run allowed install scripts explicitly
run: |
npx --yes node-gyp rebuild || true # Only if needed
- name: Audit dependencies
run: npm audit --audit-level=high
- name: Build
run: npm run build
- name: Verify no unexpected files
run: |
# Check for suspicious files added during build
git diff --name-only
if [ -n "$(git diff --name-only)" ]; then
echo "WARNING: Build modified tracked files"
git diff --name-only
exit 1
fiLa opción --ignore-scripts durante la instalación evita que los scripts de pre/post instalación se ejecuten automáticamente. Esto bloquea el vector de ataque más común en la cadena de suministro: scripts de instalación maliciosos que se ejecutan durante npm install. Ejecuta después, de forma explícita, solo los scripts concretos que necesites.
Protección en tiempo de ejecución: detección de dependencias comprometidas
Incluso con todas las medidas preventivas, una dependencia comprometida puede colarse. La monitorización en tiempo de ejecución ofrece una última línea de defensa.
// Detect suspicious runtime behavior from dependencies
const originalFetch = globalThis.fetch;
globalThis.fetch = async function monitoredFetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const url = typeof input === "string" ? input : input.toString();
// Log all outbound network requests for audit
const allowedDomains = [
"api.myapp.com",
"cdn.myapp.com",
"sentry.io",
];
const requestUrl = new URL(url);
if (!allowedDomains.some((d) => requestUrl.hostname.endsWith(d))) {
console.warn(
`[SECURITY] Unexpected outbound request: ${requestUrl.hostname}${requestUrl.pathname}`
);
// In strict mode, block the request entirely
if (process.env.STRICT_NETWORK_POLICY === "true") {
throw new Error(`Blocked request to unauthorized domain: ${requestUrl.hostname}`);
}
}
return originalFetch(input, init);
};La monitorización de red detecta paquetes comprometidos que intentan exfiltrar datos. Si un paquete que instalaste para formatear fechas empieza de repente a hacer solicitudes HTTP a servidores desconocidos, algo va mal. Generar alertas ante conexiones salientes inesperadas ofrece una alerta temprana.
Puntos clave
La seguridad de la cadena de suministro de software es un espectro, no un estado binario. Empieza por las defensas de mayor impacto: la verificación de integridad del lockfile (npm ci), los paquetes con alcance y anclaje de registro, y --ignore-scripts en las instalaciones de CI. Estas tres medidas bloquean la mayoría de los ataques conocidos a la cadena de suministro.
Añade capas de defensa adicionales según lo exija tu perfil de riesgo: anclaje de versiones exactas con PRs automatizados de actualización, auditoría de paquetes antes de instalarlos, refuerzo del pipeline de compilación con permisos mínimos y monitorización de red en tiempo de ejecución.
La verdad incómoda es que no puedes confiar plenamente en tu árbol de dependencias. Solo puedes reducir la superficie de ataque, detectar anomalías con rapidez y tener un plan de respuesta para cuando algo se cuele. Trata las dependencias como entrada externa: valida, verifica y monitoriza.


