Saltar al contenido

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.

5 min de lectura
Una cadena de candados enlazados que representa la cadena de suministro de software, desde el código fuente hasta el despliegue pasando por la compilación

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.

shbash
# ❌ 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.json

Pero 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.

tstypescript
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.

jsonjson
{
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  },
  "pnpm": {
    "onlyBuiltDependencies": [
      "esbuild",
      "sharp",
      "bcrypt"
    ]
  }
}
tstypescript
// ❌ 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.

ymlyaml
# .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.json

Lista 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.

tstypescript
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.

tstypescript
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.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX