Terug naar artikelen

Koel je packages af

Supply chain-aanvallen blijven binnenkomen. Twee simpele instellingen stoppen de meeste ervan: lockfiles en een cooldown op release-leeftijd. Zo zet je beide aan voor pip, uv, npm, pnpm en bun.

Weer een week, weer een supply chain-aanval. Deze keer was het Mini Shai-Hulud1; 172 npm- en PyPI-packages gekaapt in enkele uren. De officiele SDK van Mistral AI, TanStack Router, Guardrails AI, PyTorch Lightning. De kwaadaardige versies bleven net lang genoeg live om CI-pipelines te raken voordat de registries ze eraf trokken.

Ik checkte mijn eigen Python-omgeving. Geluk gehad. Maar "geluk gehad" is geen beveiligingsstrategie, en deze aanvallen versnellen. Er zit een patroon in en er is een verdediging, en die verdediging kost bijna niks.

De speed-run

Moderne supply chain-aanvallen zien er elke keer hetzelfde uit. Een aanvaller phisht een maintainer, kaapt hun npm- of PyPI-account en publiceert een vergiftigde versie van een package waar miljoenen mensen van afhankelijk zijn. Binnen minuten halen geautomatiseerde CI-builds over de hele wereld de nieuwe versie binnen. Binnen uren trekt de registry hem terug. Het venster is piepklein; de blast radius enorm.

In een opvallend geval was het axios-package ongeveer vier uur lang gecompromitteerd2. Dat was genoeg om een remote access trojan af te leveren bij iedereen wiens CI in dat venster draaide.

Het standaardadvies (pin je dependencies, gebruik lockfiles) is goed advies. Maar het lost maar de helft van het probleem op. Om te zien waarom, helpt het om precies te weten wat een lockfile wel en niet doet.

Hoe lockfiles eigenlijk werken

Een lockfile legt de exacte versie vast van elke package die je hebt geinstalleerd, inclusief transitieve dependencies, samen met een cryptografische hash van de bestandsinhoud. De volgende keer dat iemand install draait, haalt de package manager dezelfde versie binnen en verifieert het dat de bytes overeenkomen met de hash.

Dit beschermt je tegen een specifieke aanval: iemand die knoeit met een gepubliceerde versie die jij al hebt goedgekeurd. Als je axios@1.13.0 hebt geinstalleerd en de hash hebt vastgezet, kan een aanvaller niet zomaar een kwaadaardige 1.13.0 overheen publiceren; de hash matcht niet en je install faalt. Mooi.

Hoe lockfiles er in de praktijk uitzien:

  • JavaScript: package-lock.json (npm), pnpm-lock.yaml (pnpm), bun.lock (bun), yarn.lock (yarn). Allemaal slaan ze SHA-512 integrity hashes op voor elke resolved package.
  • Python: historisch minder gestandaardiseerd. Pipfile.lock (pipenv), poetry.lock (poetry), pdm.lock (pdm) en uv.lock (uv) doen allemaal hetzelfde. Een kale requirements.txt lockt niet standaard, maar je kunt hashes pinnen met pip install --require-hashes en de --hash=sha256:... syntax in het bestand. uv en de rest schrijven hashes automatisch weg.

Wanneer je install draait:

  1. De package manager leest de lockfile.
  2. Het downloadt de exacte versies die erin staan.
  3. Het hasht elk gedownload artifact.
  4. Het vergelijkt met de hash in de lockfile.
  5. Als een hash afwijkt, breekt de install af.

Uitstekend voor reproduceerbaarheid. Uitstekend voor het opvangen van getampered republishes. Maar let op wat een lockfile niet tegenhoudt: een nieuwe versie van een package die kwaadaardig is. Op het moment dat jij (of Renovate, of Dependabot, of een teamgenoot die npm install some-package draait) een dependency update, zit je buiten de bescherming van de lockfile en weer in het wild.

Daar komt de tweede verdediging om de hoek kijken.

Minimale release-leeftijd: de ontbrekende instelling

De meeste supply chain-aanvallen worden binnen uren of dagen na publicatie opgemerkt. De kwaadaardige versies blijven meestal niet maandenlang onopgemerkt; de community spot ze snel. De xz utils-backdoor3 is de ontnuchterende uitzondering, die ruwweg een maand lang in productie-releases sluimerde (bovenop jaren geduldige social engineering) voordat Andres Freund hem opmerkte. Maar dat niveau van geduld is zeldzaam. Dus als je gewoon weigert om iets te installeren dat in de afgelopen paar dagen is gepubliceerd, sla je het gevaarlijke venster voor de overgrote meerderheid van incidenten over.

Dit heet minimum release age (ook wel "dependency cooldown" of "release age gating"). Het is een enkele config-optie die je package manager vertelt: don't resolve any version published more recently than N days ago.

Volgens onderzoek van Andrew Nesbitt4 zou een gate van zeven dagen ruwweg de helft van de 21 grote supply chain-incidenten van het afgelopen decennium hebben tegengehouden. Goedkope verdediging, grote opbrengst.

Elke grote package manager heeft dit in de afgelopen 12 maanden toegevoegd. Het formaat en de eenheden verschillen per tool. Het idee is hetzelfde.

pip (Python, v26.1+)

In /etc/pip.conf (systeem) of ~/.config/pip/pip.conf (gebruiker):

[global]
uploaded-prior-to = P7D

pip accepteert alleen ISO 8601-duraties of absolute timestamps. P7D betekent 7 dagen; P30D zou 30 betekenen. Vriendelijke strings zoals "7 days" worden niet ondersteund5.

uv (Python, v0.9.17+)

In pyproject.toml:

[tool.uv]
exclude-newer = "7 days"

uv is wat coulanter en accepteert vriendelijke strings, ISO 8601-duraties of RFC 3339-timestamps6. Het [tool.uv] blok staat naast de rest van je projectconfig, dus het reist automatisch mee met je repo.

npm (v11.x+)

In .npmrc:

min-release-age=7

npm telt in dagen. De canonieke naam is min-release-age volgens de officiele CLI-docs7.

pnpm (v10.16+; standaard in v11)

In pnpm-workspace.yaml:

minimumReleaseAge: 10080

pnpm telt in minuten. 10080 minuten is 7 dagen. pnpm 11 zet dit standaard aan op 1440 (een dag), wat al een merkbare verbetering is; opschalen naar een week is nog veiliger8.

bun (v1.3+)

In bunfig.toml:

[install]
minimumReleaseAge = 604800

bun telt in seconden. 604800 seconden is 7 dagen9.

De twee combineren

Lockfiles en release-age gates beschermen tegen verschillende failure modes. Je wilt allebei.

  • De lockfile zegt: "once we have vetted a specific version, no one can substitute a different one without us noticing."
  • De release-age gate zegt: "when we update dependencies, never pull anything fresh enough to still be malicious."

Samen vormen ze een nette defense in depth:

  1. Dagelijkse installs draaien vanuit de lockfile. Hashes geverifieerd. Tampering uitgesloten.
  2. Wanneer je een update draait (handmatig, via Renovate of via Dependabot), bekijkt de package manager alleen versies die ouder zijn dan je cooldown.
  3. De community heeft een week gehad om elke kwaadaardige release op te pikken. De versie die je binnenhaalt is er een die de wereld al per ongeluk heeft geaudit.

Geen van beide is een silver bullet. Een geduldige aanvaller zou een sleeper-versie kunnen planten die een maand schoon blijft voordat hij activeert. Een hash-check kan een kwaadaardige eerste publish niet vangen. Maar de combinatie verhoogt de kosten van een aanval drastisch en verkleint het venster waarin een compromittering jou kan bereiken.

Wanneer je moet overriden

Soms heb je echt de verste versie nodig. Een kritieke security patch komt uit, je CI breekt door een regressie in een dependency, of je test een nieuwe release. Elke tool geeft je een ontsnappingsroute:

  • pip: pip install --uploaded-prior-to=P0D <pkg>
  • uv: per-package overrides via exclude-newer-package = { "some-pkg" = "2026-05-12" } onder [tool.uv]
  • npm: geef --min-release-age=0 mee voor het ene commando
  • pnpm: zet de package onder minimumReleaseAgeExclude in pnpm-workspace.yaml
  • bun: zet de package onder minimumReleaseAgeExcludes in bunfig.toml

Gebruik deze spaarzaam. Elke override is een uitzondering waarvoor je bewust de verantwoordelijkheid neemt.

Wat je vandaag kunt doen

Als je dit leest en je projecten hebben geen cooldown ingesteld, hier is de minimaal levensvatbare hardening, op volgorde van inspanning:

  1. Open de package manager-config van je project. Voeg de relevante regel hierboven toe. Commit het. Dat dekt je team.
  2. Zet het ook globaal op je machine (~/.npmrc, ~/.config/pip/pip.conf, etc.). Dat dekt je losse installs in willekeurige scratch-mapjes.
  3. Zorg dat je daadwerkelijk een lockfile gebruikt. Als je op een kale requirements.txt zonder hashes zit, switch naar uv of voeg --require-hashes toe met expliciete --hash regels.
  4. Documenteer de cooldown in je CONTRIBUTING of README zodat toekomstige bijdragers er niet overheen kappen zodra hun CI klaagt over een "stale" package.

De volgende Mini Shai-Hulud komt eraan. De aanvallers worden sneller, niet langzamer. Twee instellingen, tien minuten werk, en een decennium aan supply chain-incidenten krimpt met de helft. De moeite waard.

Footnotes

  1. Coverage of the Mini Shai-Hulud campaign that hit 172 packages across npm and PyPI on May 11-12, 2026 TanStack, Mistral AI, UiPath hit in fresh supply chain attack.

  2. The axios compromise stayed live for about four hours before npm pulled it Axios compromised on npm.

  3. The xz utils backdoor (CVE-2024-3094) was planted in releases 5.6.0 and 5.6.1 by long-time co-maintainer "Jia Tan" and lived in distributed tarballs for roughly a month before Andres Freund discovered it in March 2024 XZ Utils backdoor.

  4. Andrew Nesbitt's analysis of how a 7-day cooldown would have blocked 11 of 21 major supply chain incidents Package managers need to cool down.

  5. Official pip CLI docs for --uploaded-prior-to, including the ISO 8601 format requirement pip install reference.

  6. uv settings reference for exclude-newer uv exclude-newer.

  7. npm CLI v11 config reference for min-release-age npm config reference.

  8. pnpm 11.0 release notes covering minimumReleaseAge defaults pnpm 11.0.

  9. Bun 1.3 release notes introducing minimumReleaseAge Bun 1.3.