Zum Inhalt springen

Dependency-Confusion-Angriffe und wie man sie verhindert

Wie Dependency-Confusion-Angriffe Paketmanager ausnutzen, um Schadcode einzuschleusen — und welche Maßnahmen deine Lieferkette wirklich schützen.

4 Min. Lesezeit
Diagramm, das zeigt, wie ein böswilliges öffentliches Paket durch Manipulation der Versionsnummer ein privates internes Paket überschreibt

Im Februar 2021 zeigte ein Sicherheitsforscher, dass er Code in den Netzwerken von Apple, Microsoft, PayPal und Dutzenden anderer Unternehmen ausführen konnte, indem er eine Schwachstelle in der Art und Weise ausnutzte, wie Paketmanager Abhängigkeiten auflösen. Die Technik wird Dependency Confusion genannt und funktioniert, weil die meisten Paketmanager öffentliche Registries vor — oder neben — privaten durchsuchen.

Wenn deine Organisation interne Pakete in einer privaten Registry veröffentlicht, bist du verwundbar, es sei denn, du hast explizite Schritte unternommen, um das zu verhindern.

Wie der Angriff funktioniert

Der Angriff zielt auf die Lücke zwischen privaten und öffentlichen Paketregistries. Wenn ein Projekt von einer privaten Registry auf @company/auth-utils angewiesen ist, könnte der Paketmanager auch die öffentliche npm-Registry prüfen. Wenn ein Angreifer ein Paket namens auth-utils (ohne Scope) auf npm mit einer höheren Versionsnummer veröffentlicht, bevorzugen einige Konfigurationen die öffentliche Version.

jsonjson
// ❌ Vulnerable package.json — unscoped private package names
{
  "dependencies": {
    "auth-utils": "^1.2.0",
    "payment-service-sdk": "^3.0.0",
    "internal-logger": "^2.1.0"
  }
}
// An attacker publishes "auth-utils@99.0.0" to npm
// Package manager sees the higher version on the public registry
// Installs the attacker's malicious package instead
jsonjson
// ✅ Scoped packages with registry pinning
{
  "dependencies": {
    "@yourcompany/auth-utils": "^1.2.0",
    "@yourcompany/payment-service-sdk": "^3.0.0",
    "@yourcompany/internal-logger": "^2.1.0"
  }
}
// Scoped packages (@yourcompany/*) route to your private registry
// Attacker cannot claim your organization's scope on npm

Der Angriff beruht auf drei Bedingungen: unscoped interne Paketnamen, ein Paketmanager, der beide Registries abfragt, sowie keine Versionsfixierung oder Integritätsprüfungen. Entferne eine davon, und der Angriff schlägt fehl.

Verteidigungsebene 1: Alle internen Pakete mit einem Scope versehen

Die einfachste und effektivste Verteidigung besteht darin, npm-Scopes für alle internen Pakete zu verwenden. Wenn du den Scope @yourcompany in der öffentlichen npm-Registry besitzt, kann niemand sonst Pakete unter diesem Scope veröffentlichen.

shbash
# Claim your organization's scope on npm (even if you never publish there)
npm login --registry=https://registry.npmjs.org
npm org create yourcompany
# Now no attacker can publish @yourcompany/* packages publicly
tstypescript
// ❌ Internal package without scope
// package.json of your internal library
{
  "name": "feature-flags-sdk",
  "version": "1.3.0"
}
// Attacker can publish "feature-flags-sdk@99.0.0" on npm
 
// ✅ Internal package with scope
{
  "name": "@yourcompany/feature-flags-sdk",
  "version": "1.3.0"
}
// Only members of @yourcompany npm org can publish this

Selbst wenn du nie Pakete in der öffentlichen Registry veröffentlichst, beanspruche deinen Organisations-Scope defensiv. Es ist kostenlos und blockiert den häufigsten Angriffsvektor.

Verteidigungsebene 2: Registry-Konfiguration

Konfiguriere deinen Paketmanager so, dass interne Scopes an deine private Registry und alles andere an die öffentliche Registry weitergeleitet werden. Lass den Paketmanager niemals in beiden Registries nach demselben Paket "suchen".

iniini
# .npmrc — explicit registry routing
# All @yourcompany packages come from your private registry
@yourcompany:registry=https://npm.yourcompany.com/
 
# Everything else comes from the public registry
registry=https://registry.npmjs.org/
 
# Optional: require authentication for private registry
//npm.yourcompany.com/:_authToken=${NPM_PRIVATE_TOKEN}
ymlyaml
# .yarnrc.yml (Yarn Berry / Yarn 2+)
npmScopes:
  yourcompany:
    npmRegistryServer: "https://npm.yourcompany.com/"
    npmAuthToken: "${NPM_PRIVATE_TOKEN}"
 
npmRegistryServer: "https://registry.npmjs.org/"
tstypescript
// ❌ Single registry fallback — dangerous
// .npmrc with only:
// registry=https://npm.yourcompany.com/
// If private registry is down, npm falls back to public registry
// Attacker waits for an outage, or the fallback fetches malicious packages
 
// ✅ Explicit routing — no fallback ambiguity
// .npmrc with scoped registries as shown above
// @yourcompany/* → private registry (no fallback)
// everything else → public npm (no confusion)

Verteidigungsebene 3: Lockfiles und Integritätsprüfungen

Lockfiles fixieren exakte Versionen und enthalten Integritäts-Hashes. Selbst wenn ein Paketmanager eine bösartige Version auflöst, führt ein nicht übereinstimmender Integritäts-Hash zum Scheitern der Installation.

jsonjson
// package-lock.json includes integrity hashes
{
  "node_modules/@yourcompany/auth-utils": {
    "version": "1.2.0",
    "resolved": "https://npm.yourcompany.com/@yourcompany/auth-utils/-/auth-utils-1.2.0.tgz",
    "integrity": "sha512-abc123def456..."
  }
}
shbash
# CI should always use frozen lockfile installation
# npm
npm ci  # Fails if lock file is out of sync with package.json
 
# yarn
yarn install --frozen-lockfile
 
# pnpm
pnpm install --frozen-lockfile
shbash
# ❌ Running 'npm install' in CI
# This can update the lock file, potentially pulling in new versions
npm install
 
# ✅ Running 'npm ci' in CI
# This installs exactly what the lock file specifies
# Fails fast if lock file and package.json disagree
npm ci

Committe dein Lockfile in die Versionskontrolle und erzwinge npm ci (oder das Äquivalent) in allen CI-Pipelines. Jeder Entwickler, der npm install ausführt und unerwartete Änderungen am Lockfile erhält, sollte dies vor dem Commit untersuchen.

Verteidigungsebene 4: Automatisierte Audits

Führe Abhängigkeits-Audits als Teil deiner CI-Pipeline durch. Prüfe auf bekannte Schwachstellen und unerwartete Paketquellen.

ymlyaml
# GitHub Actions — dependency audit step
name: Security Audit
on:
  pull_request:
    paths:
      - 'package.json'
      - 'package-lock.json'
      - '.npmrc'
 
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
 
      - name: Install with frozen lockfile
        run: npm ci
 
      - name: Run npm audit
        run: npm audit --audit-level=high
 
      - name: Check package sources
        run: |
          # Verify all @yourcompany packages resolve to private registry
          grep -E '"resolved": "https://npm.yourcompany.com' package-lock.json \
            | wc -l
          # Verify NO @yourcompany packages resolve to public registry
          if grep -E '@yourcompany.*registry.npmjs.org' package-lock.json; then
            echo "ERROR: Internal package resolving to public registry!"
            exit 1
          fi
tstypescript
// Custom script to validate package sources
import { readFileSync } from 'fs';
 
interface PackageLockEntry {
  version: string;
  resolved: string;
  integrity: string;
}
 
function auditPackageSources(lockfilePath: string): void {
  const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf-8'));
  const packages = lockfile.packages || {};
 
  for (const [name, meta] of Object.entries<PackageLockEntry>(packages)) {
    if (name.includes('@yourcompany/')) {
      const isPrivateRegistry = meta.resolved?.startsWith(
        'https://npm.yourcompany.com'
      );
      if (!isPrivateRegistry) {
        console.error(
          `SECURITY: ${name} resolves to unexpected registry: ${meta.resolved}`
        );
        process.exit(1);
      }
    }
  }
 
  console.log('All internal packages resolve to private registry.');
}
 
auditPackageSources('package-lock.json');

Zusammenfassung: Verteidigung in der Tiefe

Eine einzelne Verteidigungsmaßnahme reicht nicht. Schichte sie:

ymlyaml
defense_layers:
  1_scoping:
    action: "Scope all internal packages under @yourcompany"
    blocks: "Attacker cannot claim your scope on public registry"
 
  2_registry_routing:
    action: "Pin scopes to specific registries in .npmrc"
    blocks: "Package manager never queries wrong registry"
 
  3_lock_files:
    action: "Use npm ci with integrity hashes in CI"
    blocks: "Even if resolution is wrong, hash mismatch stops install"
 
  4_auditing:
    action: "Automated CI checks for package source URLs"
    blocks: "Catches misconfigurations before they reach production"
 
  5_monitoring:
    action: "Alert on new packages appearing in lock file diffs"
    blocks: "Human review of unexpected dependency changes"

Wichtigste Erkenntnisse

  1. Dependency Confusion nutzt die Auflösungsreihenfolge des Paketmanagers aus — öffentliche Pakete mit höheren Versionen überschreiben private
  2. Versehe alle internen Pakete mit einem Scope (@yourcompany/*) und beanspruche den Organisations-Scope defensiv in öffentlichen Registries
  3. Binde gescopte Pakete an deine private Registry in .npmrc — lass Paketmanager niemals in beiden Registries nach demselben Namen suchen
  4. Verwende npm ci in CI-Pipelines — eingefrorene Lockfiles mit Integritäts-Hashes erkennen Auflösungsfehler
  5. Automatisiere die Quellenprüfung — verifiziere, dass interne Pakete immer in deiner privaten Registry aufgelöst werden, nicht bei npm
  6. Schichte deine Verteidigung — Scoping, Registry-Routing, Lockfiles und Audits zusammen bieten robusten Schutz
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX