Seguridad de la cadena de suministro: dependencias y confianza
Guía completa para proteger tu cadena de suministro: auditoría de dependencias, integridad del lockfile, builds endurecidos, SBOM y procedencia.

La superficie de ataque que heredas
Cada npm install incorpora cientos de paquetes que no escribiste, no revisaste y no puedes garantizar. Una sola dependencia comprometida puede inyectar criptomineros, exfiltrar variables de entorno o abrir una shell inversa. Los ataques a la cadena de suministro se han convertido en el vector preferido porque eluden todas las defensas que los desarrolladores construyen alrededor de su propio código.
Proteger la cadena de suministro implica tratar cada dependencia externa, cada paso de compilación y cada artefacto como potencialmente hostil hasta que se demuestre lo contrario.
Auditoría de dependencias e integridad del lockfile
El lockfile es la primera línea de defensa. Fija versiones exactas y registra hashes de integridad para que npm ci instale exactamente lo que se confirmó en el repositorio, no lo que el registro esté sirviendo en ese momento.
# ❌ Using npm install in CI — resolves versions dynamically
npm install
# ✅ Using npm ci — installs exactly from lockfile
npm ci --ignore-scripts
# Audit dependencies for known vulnerabilities
npm audit --audit-level=high
# Generate a detailed report
npm audit --json > audit-report.jsonPero auditar vulnerabilidades conocidas solo detecta lo que ya se ha reportado. Para los ataques de día cero en la cadena de suministro, necesitas controles más profundos.
import { execSync } from "child_process";
import { readFileSync } from "fs";
import { createHash } from "crypto";
interface LockfileCheck {
valid: boolean;
issues: string[];
}
function verifyLockfileIntegrity(lockfilePath: string): LockfileCheck {
const issues: string[] = [];
// Verify lockfile exists and isn't empty
const content = readFileSync(lockfilePath, "utf-8");
if (!content.trim()) {
return { valid: false, issues: ["Lockfile is empty"] };
}
const lockfile = JSON.parse(content);
// Check that every package has an integrity hash
for (const [name, info] of Object.entries(lockfile.packages || {})) {
const pkg = info as { integrity?: string; resolved?: string };
if (name === "") continue; // root package
if (!pkg.integrity) {
issues.push(`Missing integrity hash: ${name}`);
}
// Flag packages resolved from non-registry URLs
if (
pkg.resolved &&
!pkg.resolved.startsWith("https://registry.npmjs.org")
) {
issues.push(`Non-registry resolution: ${name} → ${pkg.resolved}`);
}
}
return { valid: issues.length === 0, issues };
}Restricción de los scripts de instalación
Muchos ataques a la cadena de suministro se ejecutan durante npm install a través de scripts de ciclo de vida como postinstall. Los paquetes legítimos usan estos scripts para compilación nativa, pero los paquetes maliciosos los usan para ejecutar código en el momento de la instalación.
{
"scripts": {
"preinstall": "npx only-allow pnpm"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp",
"bcrypt"
]
}
}// ❌ Running all install scripts blindly
// npm install (runs postinstall for every package)
// ✅ Explicitly allowlisting packages that need install scripts
interface SecurityPolicy {
allowedInstallScripts: string[];
blockedPatterns: RegExp[];
requireIntegrityHashes: boolean;
}
const policy: SecurityPolicy = {
allowedInstallScripts: [
"esbuild",
"sharp",
"@prisma/client",
"bcrypt",
],
blockedPatterns: [
/eval\s*\(/,
/child_process/,
/https?:\/\/(?!registry\.npmjs\.org)/,
],
requireIntegrityHashes: true,
};
function auditInstallScripts(
packageJsonPath: string,
policy: SecurityPolicy
): string[] {
const violations: string[] = [];
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
const scriptKeys = [
"preinstall",
"install",
"postinstall",
"prepare",
];
for (const key of scriptKeys) {
if (pkg.scripts?.[key]) {
violations.push(
`Root package has ${key} script: "${pkg.scripts[key]}"`
);
}
}
return violations;
}Fortalecimiento del pipeline de compilación
El pipeline de CI/CD es un objetivo de alto valor. Un atacante que comprometa la compilación puede inyectar código en cada artefacto sin tocar el repositorio de código fuente.
# .github/workflows/secure-build.yml
name: Secure Build
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
# Install from lockfile only
- run: npm ci --ignore-scripts
# Run only allowlisted install scripts
- run: npx --yes allow-scripts
# Verify no lockfile modifications
- name: Check lockfile integrity
run: |
git diff --exit-code package-lock.json || \
(echo "Lockfile modified during install" && exit 1)
# Build with minimal environment
- name: Build
run: npm run build
env:
NODE_ENV: production
# Generate SBOM
- name: Generate SBOM
run: npx @cyclonedx/cyclonedx-npm --output-file sbom.json
# Sign artifact with Sigstore
- name: Sign artifact
uses: sigstore/cosign-installer@v3
- run: cosign sign-blob --yes --bundle build.bundle sbom.jsonLista de materiales de software (SBOM)
Un SBOM enumera cada componente de tu artefacto compilado. Cuando ocurra el próximo Log4Shell, un SBOM te dirá en segundos si estás afectado. Sin uno, terminas ejecutando grep en tus repositorios con la esperanza de encontrar cada instancia.
interface SBOMEntry {
name: string;
version: string;
license: string;
purl: string; // Package URL standard
hashes: Record<string, string>;
dependencies: string[];
}
interface SBOM {
bomFormat: "CycloneDX";
specVersion: string;
serialNumber: string;
version: number;
components: SBOMEntry[];
metadata: {
timestamp: string;
tools: Array<{ vendor: string; name: string; version: string }>;
component: { name: string; version: string };
};
}
function analyzeSBOM(sbom: SBOM): {
totalComponents: number;
licenseBreakdown: Record<string, number>;
directDeps: number;
transitiveDeps: number;
riskFlags: string[];
} {
const licenseBreakdown: Record<string, number> = {};
const riskFlags: string[] = [];
for (const component of sbom.components) {
const license = component.license || "UNKNOWN";
licenseBreakdown[license] = (licenseBreakdown[license] || 0) + 1;
if (license === "UNKNOWN") {
riskFlags.push(`Unknown license: ${component.name}@${component.version}`);
}
// Check for known problematic licenses
if (["AGPL-3.0", "GPL-3.0", "SSPL-1.0"].includes(license)) {
riskFlags.push(
`Copyleft license ${license}: ${component.name}@${component.version}`
);
}
}
return {
totalComponents: sbom.components.length,
licenseBreakdown,
directDeps: sbom.components.filter((c) => c.dependencies.length === 0).length,
transitiveDeps: sbom.components.filter((c) => c.dependencies.length > 0).length,
riskFlags,
};
}Monitoreo de dependencias en tiempo de ejecución
El análisis estático detecta problemas conocidos en el momento de la compilación. El monitoreo en tiempo de ejecución detecta anomalías de comportamiento: paquetes que de repente empiezan a hacer solicitudes de red, acceder al sistema de archivos de formas inesperadas o leer variables de entorno que no deberían necesitar.
interface DependencyBehavior {
packageName: string;
networkRequests: string[];
fileSystemAccess: string[];
envVarsRead: string[];
childProcesses: string[];
}
function createBehaviorPolicy(
packageName: string,
expectedBehavior: Partial<DependencyBehavior>
): DependencyBehavior {
return {
packageName,
networkRequests: expectedBehavior.networkRequests || [],
fileSystemAccess: expectedBehavior.fileSystemAccess || [],
envVarsRead: expectedBehavior.envVarsRead || [],
childProcesses: expectedBehavior.childProcesses || [],
};
}
// Define expected behavior for critical dependencies
const policies: DependencyBehavior[] = [
createBehaviorPolicy("express", {
networkRequests: ["listen:*"],
fileSystemAccess: [],
envVarsRead: ["PORT", "NODE_ENV"],
childProcesses: [],
}),
createBehaviorPolicy("prisma", {
networkRequests: ["localhost:5432"],
fileSystemAccess: ["node_modules/.prisma"],
envVarsRead: ["DATABASE_URL"],
childProcesses: ["prisma-engines/*"],
}),
];
function detectAnomaly(
observed: DependencyBehavior,
policy: DependencyBehavior
): string[] {
const anomalies: string[] = [];
for (const request of observed.networkRequests) {
if (!policy.networkRequests.some((p) => matchPattern(request, p))) {
anomalies.push(
`${policy.packageName}: unexpected network request to ${request}`
);
}
}
for (const envVar of observed.envVarsRead) {
if (!policy.envVarsRead.includes(envVar)) {
anomalies.push(
`${policy.packageName}: unexpected env var access: ${envVar}`
);
}
}
return anomalies;
}Puntos clave
La seguridad de la cadena de suministro de software exige defensa en cada etapa: selección de dependencias, instalación, compilación y ejecución. Fija tus dependencias con hashes de integridad y verifica el lockfile en CI. Restringe los scripts de instalación a una lista de permitidos explícita: la mayoría de los paquetes no los necesitan.
Fortalece el pipeline de compilación usando npm ci, verificando que no haya mutaciones en el lockfile, generando SBOM y firmando los artefactos. Un SBOM no es opcional: cuando aparezca la próxima vulnerabilidad crítica, necesitas saber en minutos si estás afectado.
En tiempo de ejecución, monitorea el comportamiento de las dependencias frente a políticas definidas. Un paquete que empieza a hacer solicitudes de red inesperadas o a leer variables de entorno que antes nunca necesitaba es una señal que vale la pena investigar. La cadena de suministro es tan fuerte como su eslabón más débil, y con cientos de dependencias transitivas, ese eslabón es más pequeño de lo que crees.


