Zum Inhalt springen

Supply-Chain-Sicherheit: Abhängigkeiten, Builds, Vertrauen

Umfassender Leitfaden zur Absicherung der Software-Supply-Chain: Dependency-Audits, Lockfile-Integrität, Build-Härtung, SBOM und Herkunftsnachweis.

4 Min. Lesezeit
Eine Kette aus verbundenen Schlössern, die die Software-Supply-Chain vom Quellcode über den Build bis zum Deployment darstellt

Die Angriffsfläche, die Sie erben

Jeder npm install-Aufruf zieht Hunderte von Paketen nach sich, die Sie weder geschrieben noch geprüft haben und für die Sie nicht bürgen können. Eine einzige kompromittierte Abhängigkeit kann Krypto-Miner einschleusen, Umgebungsvariablen exfiltrieren oder eine Reverse Shell öffnen. Supply-Chain-Angriffe sind zum bevorzugten Angriffsvektor geworden, weil sie jede Verteidigung umgehen, die Entwickler um ihren eigenen Code herum aufbauen.

Die Supply Chain abzusichern bedeutet, jede externe Abhängigkeit, jeden Build-Schritt und jedes Artefakt so lange als potenziell feindlich zu behandeln, bis das Gegenteil bewiesen ist.

Dependency-Audits und Lockfile-Integrität

Die Lockfile ist die erste Verteidigungslinie. Sie fixiert exakte Versionen und speichert Integritäts-Hashes, sodass npm ci genau das installiert, was committet wurde – und nicht das, was die Registry gerade ausliefert.

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

Doch das Auditieren bekannter Schwachstellen erfasst nur das, was bereits gemeldet wurde. Für Zero-Day-Angriffe auf die Supply Chain brauchen Sie tiefergehende Kontrollen.

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 };
}

Installationsskripte einschränken

Viele Supply-Chain-Angriffe laufen während npm install über Lifecycle-Skripte wie postinstall ab. Legitime Pakete nutzen diese Skripte für native Kompilierung, doch bösartige Pakete missbrauchen sie, um bereits bei der Installation Code auszuführen.

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;
}

Härtung der Build-Pipeline

Die CI/CD-Pipeline ist ein besonders lohnendes Ziel. Ein Angreifer, der den Build kompromittiert, kann Code in jedes Artefakt einschleusen, ohne das Quell-Repository überhaupt anzurühren.

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

Software Bill of Materials (SBOM)

Ein SBOM listet jede Komponente in Ihrem gebauten Artefakt auf. Wenn der nächste Log4Shell-Vorfall eintritt, sagt Ihnen ein SBOM innerhalb von Sekunden, ob Sie betroffen sind. Ohne SBOM bleibt Ihnen nur, mit grep durch Ihre Repositories zu suchen und zu hoffen, dass Sie jede Instanz finden.

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,
  };
}

Laufzeitüberwachung von Abhängigkeiten

Statische Analyse erkennt bekannte Probleme bereits zur Build-Zeit. Laufzeitüberwachung erkennt Verhaltensanomalien – Pakete, die plötzlich Netzwerkanfragen stellen, auf ungewöhnliche Weise auf das Dateisystem zugreifen oder Umgebungsvariablen lesen, die sie eigentlich gar nicht benötigen sollten.

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;
}

Die wichtigsten Erkenntnisse

Sicherheit in der Software-Supply-Chain erfordert Verteidigung auf jeder Stufe: bei der Auswahl der Abhängigkeiten, der Installation, dem Build und der Laufzeit. Fixieren Sie Ihre Abhängigkeiten mit Integritäts-Hashes und prüfen Sie die Lockfile in der CI. Beschränken Sie Installationsskripte auf eine explizite Allowlist – die meisten Pakete brauchen sie gar nicht.

Härten Sie die Build-Pipeline, indem Sie npm ci verwenden, auf Änderungen an der Lockfile prüfen, SBOMs erzeugen und Artefakte signieren. Ein SBOM ist keine Option, die man auslassen kann – wenn die nächste kritische Schwachstelle bekannt wird, müssen Sie innerhalb von Minuten wissen, ob Sie betroffen sind.

Überwachen Sie zur Laufzeit das Verhalten Ihrer Abhängigkeiten anhand definierter Richtlinien. Ein Paket, das plötzlich unerwartete Netzwerkanfragen stellt oder Umgebungsvariablen liest, die es vorher nie brauchte, ist ein Signal, dem Sie nachgehen sollten. Die Supply Chain ist immer nur so stark wie ihr schwächstes Glied – und bei Hunderten transitiver Abhängigkeiten ist dieses Glied kleiner, als Sie denken.

Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX