Supply-Chain-Sicherheit: Abwehr von Angriffen auf Abhängigkeiten
Umfassender Leitfaden zum Schutz der Software-Lieferkette vor Dependency Confusion, Typosquatting, kompromittierten Paketen und Build-Angriffen.

Die Angriffsfläche, die man nicht sieht
Deine Anwendung besteht zu 10 % aus eigenem Code und zu 90 % aus fremdem Code. Ein typisches Node.js-Projekt zieht Hunderte transitive Abhängigkeiten nach sich. Jede einzelne ist eine Vertrauensentscheidung: Du vertraust darauf, dass der npm-Account des Maintainers nicht kompromittiert wurde, dass die Paket-Registry nicht manipuliert wurde und dass jede transitive Abhängigkeit, drei Ebenen tiefer, ebenso vertrauenswürdig ist.
Der SolarWinds-Angriff, die Kompromittierung von ua-parser-js, die Backdoor in event-stream, die Sabotage von colors.js – das sind keine theoretischen Risiken. Es sind dokumentierte Vorfälle, bei denen vertrauenswürdige Pakete zu Angriffsvektoren wurden. Supply-Chain-Sicherheit ist längst keine Option mehr, sondern eine Kernaufgabe der Softwareentwicklung.
Dependency Confusion: die Falle mit internen Paketen
Dependency Confusion nutzt aus, wie Paketmanager Namen auflösen. Wenn dein Unternehmen ein privates Paket namens @company/auth-utils verwendet, veröffentlicht ein Angreifer ein Paket mit demselben Namen im öffentlichen npm-Registry – allerdings mit einer höheren Versionsnummer. Prüft dein Paketmanager zuerst die öffentliche Registry, installiert er die Version des Angreifers.
// ❌ Bad: No registry scoping — vulnerable to dependency confusion
{
"dependencies": {
"auth-utils": "^2.0.0",
"payment-sdk": "^1.5.0"
}
}// ✅ Good: Scoped packages with registry pinning
{
"dependencies": {
"@mycompany/auth-utils": "^2.0.0",
"@mycompany/payment-sdk": "^1.5.0"
}
}# .npmrc — Pin scoped packages to your private registry
@mycompany:registry=https://npm.mycompany.com/
//npm.mycompany.com/:_authToken=${NPM_PRIVATE_TOKEN}Scoped Packages mit expliziter Registry-Bindung in .npmrc verhindern, dass der Paketmanager für deine internen Pakete überhaupt in der öffentlichen Registry nachschaut. Das ist die einfachste und wirksamste Verteidigung gegen Dependency Confusion.
Lockfile-Integrität: deine erste Verteidigungslinie
Die Lockfile fixiert exakte Abhängigkeitsversionen und enthält Integritäts-Hashes. Ändert jemand eine Abhängigkeit zwischen zwei Installationen – sei es durch eine kompromittierte Registry oder einen Supply-Chain-Angriff –, schlägt die Integritätsprüfung fehl.
// Script to verify lockfile integrity in CI
import { readFileSync } from "fs";
import crypto from "crypto";
interface LockfilePackage {
version: string;
resolved: string;
integrity: string;
}
function verifyLockfileIntegrity(lockfilePath: string): {
valid: boolean;
issues: string[];
} {
const issues: string[] = [];
const lockfile = JSON.parse(readFileSync(lockfilePath, "utf-8"));
const packages: Record<string, LockfilePackage> =
lockfile.packages || {};
for (const [name, pkg] of Object.entries(packages)) {
if (!name) continue; // Skip root
// Check for missing integrity hashes
if (!pkg.integrity) {
issues.push(`Missing integrity hash: ${name}@${pkg.version}`);
}
// Check for non-registry URLs
if (
pkg.resolved &&
!pkg.resolved.startsWith("https://registry.npmjs.org/") &&
!pkg.resolved.startsWith("https://npm.mycompany.com/")
) {
issues.push(`Unexpected registry URL: ${name} → ${pkg.resolved}`);
}
// Check for git dependencies (potential risk)
if (pkg.resolved?.startsWith("git+")) {
issues.push(`Git dependency detected: ${name} → ${pkg.resolved}`);
}
}
return { valid: issues.length === 0, issues };
}# CI pipeline step: Verify lockfile before install
- name: Verify dependency integrity
run: |
# Fail if lockfile would change (ensures it's committed and current)
npm ci --ignore-scripts
# The --ignore-scripts flag prevents pre/post install scripts from running
# during the verification phaseVerwende in der CI immer npm ci (oder bun install --frozen-lockfile), niemals npm install. Der Befehl ci schlägt fehl, wenn die Lockfile nicht zur package.json passt, und verhindert so ein stilles Auseinanderdriften der Abhängigkeiten.
Typosquatting und bösartige Pakete erkennen
Typosquatting-Pakete tragen Namen, die legitimen zum Verwechseln ähnlich sehen: lodahs statt lodash, cross-env2 statt cross-env. Automatisiertes Scannen erkennt sie, bevor sie in deinen Abhängigkeitsbaum gelangen.
// Pre-install hook to check for suspicious packages
import { execSync } from "child_process";
interface PackageAudit {
name: string;
version: string;
riskLevel: "low" | "medium" | "high" | "critical";
reasons: string[];
}
function auditNewDependency(packageName: string): PackageAudit {
const reasons: string[] = [];
let riskLevel: "low" | "medium" | "high" | "critical" = "low";
// Check package age
const info = JSON.parse(
execSync(`npm view ${packageName} --json`, {
encoding: "utf-8",
})
);
const createdDate = new Date(info.time?.created);
const ageInDays =
(Date.now() - createdDate.getTime()) / (1000 * 60 * 60 * 24);
if (ageInDays < 30) {
reasons.push(`Package is only ${Math.floor(ageInDays)} days old`);
riskLevel = "high";
}
// Check download count
const downloads = JSON.parse(
execSync(
`npm view ${packageName} --json | jq '.downloads'`,
{ encoding: "utf-8" }
).trim() || "0"
);
if (typeof downloads === "number" && downloads < 100) {
reasons.push(`Low download count: ${downloads}`);
riskLevel = riskLevel === "high" ? "critical" : "high";
}
// Check for install scripts
if (info.scripts?.preinstall || info.scripts?.postinstall) {
reasons.push("Contains install scripts");
riskLevel = "medium";
}
// Check maintainer count
const maintainers = info.maintainers || [];
if (maintainers.length === 1) {
reasons.push("Single maintainer");
}
return {
name: packageName,
version: info["dist-tags"]?.latest,
riskLevel,
reasons,
};
}Die Warnsignale: neue Pakete mit Installationsskripten, ein einzelner Maintainer, niedrige Downloadzahlen und Namen, die populären Paketen ähneln. Kein einzelnes Signal ist für sich genommen beweiskräftig, aber die Kombination ergibt ein klares Bild.
Versionen fixieren und Updates automatisieren
Versionsbereiche (^ und ~) erlauben automatische Minor- und Patch-Updates – und genau so verbreiten sich kompromittierte Versionen. Das Fixieren exakter Versionen gibt dir die Kontrolle darüber, wann Updates stattfinden.
// ❌ Bad: Version ranges allow silent updates
{
"dependencies": {
"express": "^4.18.0",
"lodash": "~4.17.0"
}
}// ✅ Good: Exact versions with automated update PRs
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21"
}
}# Renovate config for controlled dependency updates
# renovate.json
{
"extends": ["config:base"],
"rangeStrategy": "pin",
"schedule": ["after 10pm every weekday", "before 5am every weekday"],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"schedule": ["at any time"]
},
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"automergeType": "pr",
"requiredStatusChecks": ["ci"]
},
{
"matchDepTypes": ["dependencies"],
"automerge": false,
"reviewers": ["team:security"]
}
]
}Fixiere exakte Versionen und nutze anschließend Renovate oder Dependabot, um Pull Requests für Updates zu erstellen. Dev-Abhängigkeiten können nach erfolgreicher CI automatisch gemergt werden. Produktionsabhängigkeiten erfordern eine menschliche Prüfung. Sicherheitslücken lösen unabhängig vom Zeitplan sofort einen PR aus.
Build-Pipeline härten
Die Build-Pipeline ist eine weitere Angriffsfläche. Kompromittiert ein Angreifer deine CI-Umgebung, kann er während des Build-Prozesses Code einschleusen – selbst wenn dein Quellcode sauber ist.
# GitHub Actions with security hardening
name: Build and Deploy
on:
push:
branches: [main]
permissions:
contents: read # Minimum required permissions
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false # Don't leak tokens
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: ".node-version"
- name: Install dependencies
run: npm ci --ignore-scripts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Run allowed install scripts explicitly
run: |
npx --yes node-gyp rebuild || true # Only if needed
- name: Audit dependencies
run: npm audit --audit-level=high
- name: Build
run: npm run build
- name: Verify no unexpected files
run: |
# Check for suspicious files added during build
git diff --name-only
if [ -n "$(git diff --name-only)" ]; then
echo "WARNING: Build modified tracked files"
git diff --name-only
exit 1
fiDie Option --ignore-scripts bei der Installation verhindert, dass Pre-/Post-Install-Skripte automatisch ausgeführt werden. Das blockiert den häufigsten Angriffsvektor in der Supply Chain: bösartige Installationsskripte, die während npm install laufen. Führe im Anschluss nur die konkret benötigten Skripte gezielt und explizit aus.
Laufzeitschutz: kompromittierte Abhängigkeiten erkennen
Selbst mit allen präventiven Maßnahmen kann eine kompromittierte Abhängigkeit durchrutschen. Laufzeitüberwachung bietet eine letzte Verteidigungslinie.
// Detect suspicious runtime behavior from dependencies
const originalFetch = globalThis.fetch;
globalThis.fetch = async function monitoredFetch(
input: RequestInfo | URL,
init?: RequestInit
): Promise<Response> {
const url = typeof input === "string" ? input : input.toString();
// Log all outbound network requests for audit
const allowedDomains = [
"api.myapp.com",
"cdn.myapp.com",
"sentry.io",
];
const requestUrl = new URL(url);
if (!allowedDomains.some((d) => requestUrl.hostname.endsWith(d))) {
console.warn(
`[SECURITY] Unexpected outbound request: ${requestUrl.hostname}${requestUrl.pathname}`
);
// In strict mode, block the request entirely
if (process.env.STRICT_NETWORK_POLICY === "true") {
throw new Error(`Blocked request to unauthorized domain: ${requestUrl.hostname}`);
}
}
return originalFetch(input, init);
};Netzwerküberwachung entlarvt kompromittierte Pakete, die versuchen, Daten zu exfiltrieren. Wenn ein Paket, das du für die Datumsformatierung installiert hast, plötzlich HTTP-Anfragen an unbekannte Server stellt, stimmt etwas nicht. Alarmierung bei unerwarteten ausgehenden Verbindungen liefert eine Frühwarnung.
Das Wichtigste in Kürze
Software-Supply-Chain-Sicherheit ist ein Spektrum, kein binärer Zustand. Beginne mit den wirkungsvollsten Maßnahmen: Lockfile-Integritätsprüfung (npm ci), Scoped Packages mit Registry-Bindung und --ignore-scripts bei CI-Installationen. Diese drei Maßnahmen blockieren den Großteil der bekannten Supply-Chain-Angriffe.
Ergänze weitere Verteidigungsebenen, wie es dein Risikoprofil erfordert: exaktes Versions-Pinning mit automatisierten Update-PRs, Paketprüfung vor der Installation, gehärtete Build-Pipelines mit minimalen Berechtigungen und Netzwerküberwachung zur Laufzeit.
Die unbequeme Wahrheit ist: Du kannst deinem Abhängigkeitsbaum nicht vollständig vertrauen. Du kannst nur die Angriffsfläche verkleinern, Anomalien schnell erkennen und einen Reaktionsplan für den Ernstfall bereithalten. Behandle Abhängigkeiten wie externe Eingaben – validiere, verifiziere und überwache sie.


