NPM — du vet ikke alltid hva du får!

Terje Karlsen
Systek
Published in
6 min readAug 30, 2023

Siden NPM (Node Package Manager) ble gjort tilgjengelig for ivrige utviklerfingre i 2010 har dette økosystemet vokst kraftig og blitt helt uunnværlig i mange prosjekter. Men ikke alle er klar over hvilken risiko vi løper hver eneste dag, bare på grunn av en liten kommando: npm install.

NPM er i grunnen overraskende tillitsbasert

NPM bygger i stor grad på tillit. Riktignok kan jeg som utvikler utforske koden bak en NPM-pakke før jeg velger å ta den inn i prosjektet mitt, men det er både tidkrevende og lite presist i en stor kodebase.

I det store og hele sjekker jeg derfor bare 3 ting før jeg velger å bruke en ny pakke:

  1. Hvor mange installasjoner har pakken pr uke? Med andre ord, hvor mange bruker den allerede?
  2. Hvilket versjonsnummer er pakken på? Med andre ord, hvor moden og stabil kan jeg håpe at pakken er?
  3. Når var siste endring og versjonering? Med andre ord, er det noen som aktivt vedlikeholder pakken?

Npmjs.com gir meg heldigvis godt hjelp når jeg forsøker å ettergå en ny pakke i sømmene, men utover det jeg har nevnt, stoler jeg som oftest på at eksempelvis 14 millioner nedlastinger pr uke for dayjs vil være med å avdekke om det er noe muffins med pakken før jeg installerer den selv.

Hvis jeg allerede bruker en pakke og det kommer en oppdatering blir utfordringen en annen:

  • Jeg forsøker å sjekke eventuelle endringslogger for “breaking changes” hvis de i det hele tatt finnes.
  • Jeg sjekker at det iallfall er noen dager siden oppdateringen kom, i håp om at andre har oppdaget feil.
  • Utover det oppgraderer jeg lokalt og “ser hva som går i stykker”..

En slik tillitsbasert ordning fungerer finfint — helt til det smeller.

…så hva kan gå 💩?

NPM life cycle scripts: preinstall og postinstall

Når vi installerer en ny pakke, kjører vi som oftest npm install [pakkenavn]. NPM tilbyr såkalte life cycle scripts og “install hooks” som gjør at vi kan angi handlinger og operasjoner som NPM skal gjøre før eller etter at pakken er installert. Intensjonen er selvsagt forarbeid (preinstall) eller opprydding (postinnstall), men det er lett å se for seg at slike “install hooks” kan misbrukes.

Eksempelet nedenfor viser hvordan man kan sette opp postinstall til å kjøre et ekstra script via node som deretter kan forsøke å fiske ut data fra filer i prosjektet slik som tokens. Vi kikker på et konkret eksempel på dette senere.

"name: "seemingly-trusting-package",
"scripts": {
"postinstall": "node malicious.js"
}

Patch som ikke er en patch

Har du noen gang oppgradert en avhengighet som bare er en patch uten å sjekke så mye mer detaljert? Det har iallfall jeg… Vanlig semver-praktisk indikerer tross alt at en slik patch, altså mindre justeringer og rettelser ikke inneholder store endringer, men siden dette er en konvensjon og ikke en garantert regel så kan mye ny kode skjule seg bak en enkel versjonsbump fra 4.5.3 til 4.5.4.

En patch kan altså inneholde både store endringer, men også endringer som en ondsinnet utvikler håper går under radaren til de som allerede har installert pakken — nettopp fordi det bare er en patch.

Typo squatting

Typo squatting er en teknikk hvor man publiserer en NPM-pakke under et navn som likner på kjente pakker. Ingen typo squattere i eksempelet nedenfor eksisterer lenger, men gir likevel et bilde på hvor lett det er å tråkke feil med npm install settinskriveleifher.

Legitim pakke

Typo squatter

cross-env => crossenv
mongoose => mongose
@azure/core-tracing => core-tracing

Og selv om man raskt oppdager at man har installert feil pakke kan skaden allerede være skjedd: vi har sluppet ondsinnet kode inn i det lokale miljøet vårt, kode som allerede kan ha kjørt via postinstall som jeg beskrev ovenfor.

Eksempler fra jungelen

Frem til nå har jeg skrevet om teoretiske sikkerhetsrisiko, så la oss ta en titt på eksempler fra jungelen!

Peacenotwar — eksempel på supply chain attack

I mars 2022 publiserte Brandon Nozaki Miller npm-pakken peacenotwar som forsøkte å spore geoposisjonen til maskinen som kjørte NPM-pakken. Dersom dette viste seg å være en maskin plassert i Russland eller Belarus, forsøkte koden å erstatte en rekke filer i systemet med unicode hjerte-emoji. Få dager senere publiserte Miller riktignok en oppgradering som nøyde seg med å lagre én fil til skrivebordet dersom filen ikke allerede eksisterte.

Pakken fikk først utbredelse da Miller la den inn i node-ipc, som han også var forfatter av. Dette er en NPM-pakke for kommunikasjon mellom prosesser i NodeJs, og denne pakken brukes av flere store og kjente NPM-pakker, blant annet VueJS og Unitys spillmotor. Først nå fikk pakken virkelig stor spredning.

Sikkerhetsbristen ble oppdaget etter kort tid, og selv om de fleste vil kunne si seg enig i selve budskapet om støtte for Ukraina, så ble metoden og koden selvsagt betegnet som ondsinnet og noe resten av utviklersamfunnet tok avstand fra.

Eksempelet viser først og fremst hvordan enkeltpersoner kan være i stand til å få plassert ondsinnet kode i tilsynelatende små pakker, som igjen konsumeres av større rammeverk og applikasjoner med flere millioner nedlastinger i uken.

Eslint scope pakkeangrep

I juni 2018 greide noen å bryte seg inn i en NPM-konto og publisere ondsinnet kode til pakken eslint-scope og publiseringen ble versjonbumpet som en patch. Som jeg diskuterte tidligere er terskelen for å oppgradere patcher relativt lav hos mange utviklere, inkludert meg selv.

Den ondsinnede koden forsøkte å tilegne seg autentiserings-tokens fra filen .npmrc lokalt på maskinen som kjørte koden. Denne token kunne i teorien bli brukt til å få tilgang til andre NPM-konti som ikke hadde 2FA påslått. I dag er tofaktorautentisering (2FA) ikke bare anbefalt, men også et krav i mange bedrifter, men det var ikke tilfelle i 2018 og heller ikke særlig utbredt på den tiden.

Hendelsen viser hvordan selv tilsynelatende enkle patch-oppdateringer kan inneholde skadelig kode, og at semver-konseptet i seg selv ikke er noen garanti for hva som faktisk skjuler seg bak en oppdatering. I tillegg er dette en påminnelse om å skru på 2FA så mange steder som mulig.

Så hva kan man gjøre?

Sikkerhetshendelser som jeg nevnte ovenfor avdekkes relativt raskt og det gjøres stadig nye tiltak for at NPM skal bli sikrere, men utviklere og grupper med ondsinnede hensikter ligger alltid et par skritt foran. Derfor er det enkle grep utviklere og team kan gjøre selv:

Eksakt versjonering

I dag er det vanlig å låse ned versjoner på avhengigheter med lock-files. Likevel kan det være verdt å diskutere hvorvidt vi skal operere med absolutt versjonering når vi henter inn avhengigheter i et prosjekt.

Det vanlige er å bruke “hatt” / “charet” for å angi at vi ønsker å angi en bredde hvor NPM kan oppgradere innenfor minor eller minor-versjoner. Feks slik: react: “^18.2.0”. Dette gjør at NPM vil kunne oppdatere alle patch- og minorreleaser så lenge de er innenfor major-release “18”.

Ved å praktisere eksakt versjonering tvinger du i grunnen deg selv til å gjøre et bevisst valg ved oppgradering av en patch. Det er litt mer jobb, men du har mer kontroll.

Gode prosesser

I mitt team har vi fått på plass bedre prosesser for oppgradering av avhengigheter. Denne jobben gjøres som en egen oppgave i kanban-boardet vårt, og vi lager i tillegg egen PR for godkjenning. Det settes av tid til å gjøre slike vedlikeholdsoppgaver, og det er også takhøyde for at det tar tid å sikre at oppgraderingene fungerer som de skal, se etter breaking changes og gjøre diverse sjekk. Spesielt gjelder det de mer avanserte applikasjonene med mange avhengigheter og kompleks arkitektur.

Det vi som team ønsket å unngå var store PR’er med kode hvor versjonsoppdateringer ble sneket inn fordi utvikleren valgte å “like gjerne ta det samtidig når hen først var i gang.” Da er det lett å miste oversikten og slippe igjennom pakkeversjoner som i beste fall har breaking changes og i verste fall har ondsinnet kode.

Verktøy

Det finnes flere verktøy for automatisering og monitorering av avhengigheter i en applikasjon. Verktøyene kan gi sikkerhetsrapporter, varsle om nye versjoner etc. Følg med på del 2 av denne serien som tar for seg interessante verktøy laget for å bedre sikkerheten til team og gjøre håndtering av avhengigheter tryggere.

Viktig takeaway

  • Avhengigheter, særlig i frontend baserer seg i stor grad på tillit. Kommer det en ny patch-versjon av en pakke du allerede bruker er terskelen lav for å oppgradere uten å ettergå pakken nevneverdig i sømmene.
  • Det er ikke nødvendigvis pakken du bruker som kan forårsake sikkerhetsbrist, men underliggende avhengigheter som denne pakken i sin tur bruker.
  • En patch er ikke alltid en patch — det avhenger av at utvikleren bak pakken holder seg til semver-praksisen og er ærlig på hvor mange endringer den nye versjonen faktisk inneholder.

Har du spørsmål, innspill eller ønsker å vite mer, vil vi gjerne høre fra deg :-)
Mail: systek@systek.no

Psssst: nysgjerrig på en karriere hos oss? Ta en titt her: https://www.systek.no/jobb-i-systek

--

--

Terje Karlsen
Systek
Writer for

Developer at Systek, Oslo. Enjoys coding, all things gadget, whiskey, running and cooking.