Hoppa till innehåll

Supplychain-attackermotnpm:Vadpolyfill.io-incidentenavslöjadeomJavaScript-ekosystemetssäkerhet

Den 25 juni 2024 publicerade säkerhetsföretaget Sansec en rapport som fick stora delar av webbutvecklingsvärlden att stanna upp. Domänen polyfill.io, som levererade polyfills till webbläsare via ett CDN, hade börjat injicera skadlig JavaScript-kod i de svar som skickades till besökare. Koden redirectade mobila användare till betting-sajter och scam-sidor. Över 100 000 webbplatser inkluderade polyfill.io-skriptet via en <script>-tagg, och alla drabbades samtidigt.

Det var inte ett intrång. Ingen hackade sig in i en server. Domänen och det tillhörande GitHub-repot hade sålts av den ursprungliga skaparen, Andrew Betts (tidigare utvecklare på Financial Times), till ett kinesiskt företag som kallades Funnull. Köpet skedde i februari 2024. Fyra månader senare började den skadliga koden levereras.

Det här är en supply chain-attack. Och den illustrerar ett grundläggande problem i hur JavaScript-ekosystemet hanterar förtroende.

Polyfill.io: händelseförloppet i detalj

Andrew Betts skapade polyfill.io 2014 som ett open source-projekt. Tanken var enkel: webbplatser inkluderade en <script>-tagg som pekade mot cdn.polyfill.io, och CDN:et levererade exakt de polyfills som besökarens webbläsare behövde. Om du surfade med en modern Chrome fick du i princip en tom fil. Om du surfade med Internet Explorer 11 fick du en bundel med polyfills för Promise, fetch, Array.from och andra saknade API:er.

Projektet blev populärt. 2024 laddades polyfill.io-skriptet av hundratusentals sajter. Betts hade vid det laget länge sagt att tjänsten inte längre behövdes, eftersom moderna webbläsare stödjer de API:er som polyfills fyllde i. Men <script>-taggen satt kvar i otaliga kodbaser.

I februari 2024 dök ett företag vid namn Funnull upp och köpte domänen polyfill.io samt det tillhörande GitHub-kontot. Betts reagerade och twittade en varning: "om din sajt använder polyfill.io, ta bort det omedelbart." Men varningen nådde inte alla.

Den 25 juni publicerade Sansec sin analys. Den skadliga koden som levererades via polyfill.io var inte dum. Den kontrollerade User-Agent-headern och aktiverades bara för mobila enheter. Den kontrollerade tidpunkten och körde bara under vissa timmar. Den exkluderade requests som verkade komma från analysverktyg eller säkerhetsscannrar. Koden genererade en redirect via en falsk Google Analytics-domän (www.googie-anaiytics.com, där bokstaven L ersatts med I) som skickade användaren till en betting-sajt eller scam-sida.

Bland de drabbade sajterna fanns JSTOR, Intuit, World Economic Forum och tiotusentals mindre sajter. Google reagerade genom att blockera annonser på sidor som laddade polyfill.io-skriptet, vilket innebar att sajter med Google Ads plötsligt förlorade annonsintäkter.

Cloudflare och Fastly agerade snabbt. Cloudflare hade efter ägarbytet i februari 2024 satt upp en alternativ polyfill-endpoint via sin cdnjs-tjänst, och efter att den skadliga koden upptäcktes satte de upp automatisk rewriting av polyfill.io-requests för kunder som använde Cloudflare. Fastly lanserade en liknande mirror via polyfill-fastly.io. Namecheap, domänregistraren, tog till slut ner polyfill.io-domänen helt.

Vad polyfill.io-incidenten avslöjar

Problemet var inte tekniskt komplicerat. Ingen zero-day-sårbarhet utnyttjades. Ingen kryptering bröts. En person sålde sin domän, den nya ägaren ändrade vad domänen levererade. Det var allt.

Det avslöjar något viktigt: när du inkluderar en tredjepartsresurs via en <script src="...">-tagg ger du den domänens ägare full kontroll över vad som körs i dina besökares webbläsare. Det finns inget versionslås, ingen hashverifiering (om du inte använder SRI), ingen granskning. Varje page load hämtar den senaste versionen, och om ägaren ändrar den, får dina besökare den ändrade versionen.

Det gäller inte bara polyfill.io. Det gäller varje CDN-hosstad resurs som du inkluderar utan Subresource Integrity.

Supply chain-attacker i npm: bredare mönster

Polyfill.io var en CDN-baserad attack, men samma princip gäller npm-paket. JavaScript-ekosystemet är byggt på ett enormt dependency-träd. En genomsnittlig Next.js-applikation har hundratals transitive dependencies. Varje dependency är en förtroenderelation: du litar på att paketet gör vad det säger, att maintainern inte injicerar skadlig kod, och att ingen kapade maintainerns konto.

Historien visar att det förtroendet bryts regelbundet.

event-stream och flatmap-stream (2018)

I november 2018 upptäckte en användare att npm-paketet event-stream (som hade runt 2 miljoner nedladdningar per vecka) innehöll skadlig kod. Kedjan var följande: den ursprungliga maintainern, Dominic Tarr, hade tappat intresset för paketet. En användare vid namn right9ctrl erbjöd sig att ta över underhållet. Tarr accepterade och överlämnade npm-publiseringsrättigheter.

Den nya maintainern lade till ett nytt dependency: flatmap-stream. Paketet såg oskyldigt ut vid en snabb granskning. Men i den minifierade koden fanns en krypterad payload som aktiverades enbart i miljön för Copay, en Bitcoin-wallet-app utvecklad av BitPay. Koden stal privata nycklar från Copay-användare.

Attacken var riktad, sofistikerad och gick oupptäckt i över två månader. Den illustrerade ett mönster som återkommer: maintainers av populära open source-paket brinner ut, lämnar över till någon annan, och den nya personen har andra avsikter.

ua-parser-js (oktober 2021)

Den 22 oktober 2021 kapades npm-kontot för Faisal Salman, maintainer av ua-parser-js. Paketet hade vid den tidpunkten runt 8 miljoner nedladdningar per vecka och användes av stora företag som Facebook, Amazon och Microsoft. Tre komprometterade versioner publicerades: 0.7.29, 0.8.0 och 1.0.0.

De skadliga versionerna innehöll ett postinstall-script som laddade ner och körde en cryptominer (för Monero) på Linux och en trojansk lösenordsstjälare (DanaBot) på Windows. CISA utfärdade en varning samma dag. Komprometterade versioner var aktiva i ungefär fyra timmar innan de togs bort. Under de fyra timmarna installerades paketet av tusentals projekt.

Incidenten spåras via GitHubs säkerhetsrådgivning GHSA-pjwm-rvh2-c87w.

colors och faker (januari 2022)

I början av januari 2022 publicerade Marak Squires, skapare av de populära npm-paketen colors (20+ miljoner nedladdningar/vecka) och faker (2,8 miljoner nedladdningar/vecka), nya versioner som bröt all funktionalitet. colors skrevs om för att skriva ut en oändlig loop av meningslösa tecken i terminalen. faker tömdes helt.

Det var inte en extern attack utan en protest från maintainern själv, som var frustrerad över att stora företag använde hans open source-arbete utan att bidra ekonomiskt. Incidenten är inte en supply chain-attack i traditionell mening, men effekten var densamma: tusentals projekt gick sönder utan förvarning. Den väckte en bred diskussion om open source-underhåll, burnout och företagens ansvar.

Typosquatting: stavfel som attack-vektor

Typosquatting är en av de enklaste formerna av supply chain-attack. En angripare registrerar ett npm-paket med ett namn som liknar ett populärt paket och hoppas att utvecklare skriver fel.

Dokumenterade exempel:

  • crossenv (istället för cross-env): Upptäcktes 2017. Paketet samlade in miljövariabler (inklusive npm_config-tokens) och skickade dem till en extern server.
  • colourama (istället för colorama, ett Python-paket): Skadlig kod som stal kryptovaluta-wallets.
  • electorn (istället för electron): Samlade in IP-adresser, geolokalisering och enhetsdata.
  • loadsh (istället för lodash): Registrerat med skadligt postinstall-script.

npm har implementerat åtgärder mot typosquatting, bland annat varningar när ett paketnamn liknar ett populärt paket. Men systemet är inte heltäckande. I mars 2023 rapporterade Socket.dev att de identifierade över 1 000 nya typosquatting-paket under en enda vecka.

Dependency confusion (2021)

I februari 2021 publicerade säkerhetsforskaren Alex Birsan en artikel som beskrev en ny attack-vektor: dependency confusion. Principen är enkel. Många företag har interna npm-registries med privata paket. Om ett privat paket heter company-utils och du inte har registrerat det namnet på det publika npm-registryt, kan en angripare göra det.

När npm resolver dependencies kollar det både det privata och det publika registryt. Om det publika registryt har en nyare version av company-utils, installeras den publika versionen istället för den privata. Angriparen publicerar version 99.0.0 med skadlig kod, och npm väljer den versionen.

Birsan testade attacken mot 35 stora teknikföretag, inklusive Apple, Microsoft, PayPal, Shopify, Netflix och Yelp. Han lyckades köra kod på interna byggsystem hos flera av dem. Han fick bug bounty-belöningar på totalt över 130 000 USD.

Slopsquatting: AI-hallucineringar som attack-vektor

Ett nyare fenomen, identifierat under 2024 och 2025, är slopsquatting. AI-modeller som genererar kod hallucinerar ibland paketnamn som inte existerar. Om en modell föreslår npm install fast-xml-validator men det korrekta paketet heter fast-xml-parser, kan en angripare registrera fast-xml-validator med skadlig kod.

Forskare från Socket.dev och andra säkerhetsföretag har dokumenterat att AI-kodassistenter konsekvent producerar samma felaktiga paketnamn, vilket gör dem förutsägbara nog att utnyttja. En studie från mars 2025 visade att runt 5,2% av paketnamn som AI-kodassistenter genererade inte existerade på npm.

Vad lockfiles skyddar mot, och inte

En vanlig uppfattning är att lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) skyddar mot supply chain-attacker. Det stämmer delvis.

Vad lockfiles gör

En lockfile pinnar exakta versioner av varje dependency och transitive dependency. Om du kör npm install och en lockfile finns, installerar npm exakt de versioner som anges i lockfilen. Utan lockfile tolkar npm semver-ranges i package.json (till exempel ^4.17.0) och installerar den senaste kompatibla versionen.

Det betyder att om du har en lockfile som pinnar lodash@4.17.21, och version 4.17.22 publiceras (även om den innehåller skadlig kod), får du fortfarande 4.17.21 vid install.

Vad lockfiles inte skyddar mot

Lockfiles hjälper inte om den version som redan är pinnad i lockfilen blir komprometterad innan du installerar den. Om en maintainers konto kapas och en skadlig version publiceras som den version du redan har pinnad, skyddar inte lockfilen i sig (däremot fångar integritets-hashen i lockfilen ändringar om du använder npm ci). En lockfile hjälper inte heller mot attacker som sker vid initial install, alltså första gången du lägger till ett dependency.

Lockfiles skyddar inte mot skadliga postinstall-scripts i dependencies som redan finns i lockfilen. Om du kör npm install och ett dependency har ett postinstall-script, körs det oavsett lockfile.

npm ci vs npm install

I en CI/CD-miljö ska du alltid använda npm ci istället för npm install. Skillnaden: npm ci tar bort hela node_modules-mappen, installerar exakt vad lockfilen säger, och misslyckas om package.json och lockfilen inte stämmer överens. npm install kan uppdatera lockfilen om det finns avvikelser, vilket innebär att din CI-build kan installera andra versioner än vad utvecklaren testade lokalt.

Att npm ci misslyckas vid lockfile-avvikelser är en funktion, inte en bugg. Det fångar fall där någon ändrat package.json utan att uppdatera lockfilen.

Konkreta skyddsåtgärder mot supply chain-attacker

Det finns inget enskilt verktyg som eliminerar risken, men en kombination av åtgärder minskar attackytan avsevärt.

npm audit och dess begränsningar

npm audit checkar dina dependencies mot GitHubs Advisory Database. Det fångar kända sårbarheter med tilldelade CVE-nummer. Men det missar:

  • Noll-dagars-attacker (per definition okända)
  • Skadliga paket som inte rapporterats ännu
  • Typosquatting-paket (de är inte sårbara, de är avsiktligt skadliga)
  • Komprometterade maintainer-konton (den publicerade versionen ser legitim ut)

npm audit producerar också en hel del brus. Många rapporterade sårbarheter gäller dev-dependencies som aldrig körs i produktion, eller transitiva dependencies som du inte kan uppgradera utan att byta huvudberoendet. Det leder till att team ignorerar npm audit-output, vilket är värre än att inte köra det alls.

Subresource Integrity (SRI) för CDN-resurser

Om du laddar JavaScript via en CDN-tagg (som polyfill.io-fallet), kan Subresource Integrity förhindra att ändrad kod exekveras. SRI fungerar genom att du anger en kryptografisk hash av den förväntade filen:

<script
  src="https://cdn.example.com/library.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous"
></script>

Om CDN:et levererar en fil vars hash inte matchar, blockerar webbläsaren skriptet. Det hade helt förhindrat polyfill.io-attacken, om sajterna hade använt SRI.

Begränsningen med SRI är att det inte fungerar för dynamiska resurser. Polyfill.io levererade olika innehåll beroende på User-Agent, vilket gör det svårt att ha en enda hash. Men för statiska bibliotek som jQuery, React eller Lodash är SRI ett enkelt och effektivt skydd.

Dependency scanning: Socket.dev, Snyk och Dependabot

Tre verktyg som går längre än npm audit:

Socket.dev analyserar paketens beteende snarare än bara kända CVE:er. Det letar efter nätverksanrop i postinstall-scripts, obfuskerad kod, åtkomst till filsystemet, och andra mönster som indikerar skadlig avsikt. Socket.dev integreras med GitHub och flaggar suspekta dependencies direkt i PR-reviews.

Snyk erbjuder sårbarhetsskanning med en egen databas som ofta har rapporter innan de dyker upp i npm audit. Snyk kan också föreslå säkra versionsuppgraderingar och skapa PR:er automatiskt.

GitHub Dependabot skapar automatiskt PR:er som uppgraderar dependencies med kända sårbarheter. Det är gratis för alla GitHub-repos och kräver minimal konfiguration. Begränsningen är att Dependabot bara reagerar på kända CVE:er, inte beteendeanalys.

npm provenance: verifiering av paketens ursprung

npm provenance, lanserat i april 2023, använder Sigstore för att kryptografiskt knyta ett publicerat npm-paket till en specifik build i ett CI/CD-system. Det betyder att du kan verifiera att version 2.1.0 av ett paket verkligen byggdes från commit abc123 i ett specifikt GitHub-repo, och inte laddades upp manuellt från någons laptop.

Du kan kontrollera provenance för ett paket med:

npm audit signatures

Det här skyddar mot scenariot där en maintainers npm-token stjäls och används för att publicera en komprometterad version. Om paketet har provenance och den komprometterade versionen inte byggdes via det förväntade CI-systemet, saknas signaturen.

Provenance är valfritt. I augusti 2024 hade runt 15% av de mest nedladdade npm-paketen provenance aktiverat. Andelen växer, men långsamt.

Scoped packages: @organisation/paketnamn

npm-paket som använder en scope (prefix med @) är svårare att typosquatta. @angular/core kan bara publiceras av ägaren av npm-organisationen angular. Det finns ingen risk att någon registrerar @angular/croe.

Om du publicerar egna paket, använd alltid en scope. Om du konsumerar paket, föredra scoped packages där alternativet finns. Det eliminerar inte alla risker men gör typosquatting och dependency confusion svårare.

För dependency confusion specifikt: registrera dina interna paketnamn på det publika npm-registryt som tomma platshållare. Det förhindrar att en angripare tar namnet.

Minimera dependency-trädet

Varje dependency är en attack-yta. Paketet du installerar är inte hela bilden. Ett paket med 5 direkta dependencies som var och en har 10 transitive dependencies ger dig 55 paket att lita på.

Fråga dig själv före varje npm install:

  • Kan jag lösa det här med standardbiblioteket? fetch finns inbyggt i Node.js 18+. crypto finns inbyggt. path, fs, url finns inbyggt.
  • Är det här paketet värt sin dependency-kedja? Paketet is-odd (som checkar om ett tal är udda) har runt 500 000 nedladdningar per vecka. Det gör exakt: return n % 2 === 1. Det behöver inte vara ett paket.
  • Finns det ett lättare alternativ? Behöver du hela lodash (1,4 MB utpackat) för en enda funktion, eller kan du importera lodash.get separat, eller skriva de tre raderna själv?

Att minska antalet dependencies är den mest grundläggande säkerhetsåtgärden. Färre dependencies innebär färre maintainers att lita på, färre postinstall-scripts som körs, och en mindre attackyta.

Pinning och reproducible builds

Utöver lockfiles kan du pinna exakta versioner i package.json genom att ta bort caret (^) och tilde (~) prefix:

{
  "dependencies": {
    "next": "14.2.5",
    "react": "18.3.1"
  }
}

Det förhindrar att npm install utan lockfile hämtar en nyare version. Kombinerat med en lockfile och npm ci ger det reproducerbara builds.

Corepack (inbyggt i Node.js 16+) kan dessutom pinna pakethanterarens version. Genom att ange "packageManager": "npm@10.8.1" i package.json säkerställer du att alla i teamet och CI-miljön använder samma version av npm.

Inaktivera postinstall-scripts

Många supply chain-attacker använder postinstall-scripts för att köra skadlig kod direkt vid installation. Du kan inaktivera dem globalt:

npm config set ignore-scripts true

Det kräver att du manuellt kör scripts för paket som behöver dem (till exempel node-gyp för nativa tillägg), men det blockerar automatisk exekvering av okänd kod vid install. Alternativt kan du använda --ignore-scripts som flagga till npm ci i CI/CD-pipelines och sedan explicit köra de scripts du vet behövs.

Hur team bör tänka kring dependencies

Tekniska skyddsåtgärder räcker inte om kulturen i teamet behandlar npm install som något trivialt. Varje paket du lägger till i ditt projekt innebär att du delegerar förtroende till en person eller grupp du förmodligen aldrig träffat.

Granska maintainers vid uppgradering

När du uppgraderar ett dependency, kolla vem som publicerade den nya versionen. På npm kan du se publiceringsinformation med npm info paketnamn. Om maintainern har bytts nyligen, ta en extra titt på ändringarna. event-stream-attacken hade kunnat upptäckas om någon reagerat på att en okänd person tog över publishrättigheterna.

Interna npm-registries

För företag med känslig kod och höga säkerhetskrav är ett internt npm-registry (som Artifactory, Nexus eller Verdaccio) ett effektivt skydd. Alla paket som installeras hämtas via det interna registryt, som fungerar som en proxy mot det publika npm-registryt. Teamet kan konfigurera policyer: tillåt bara paket med provenance, blockera paket med kända sårbarheter, kräv manuellt godkännande av nya dependencies.

Det interna registryt löser också dependency confusion: om det interna registryt har company-utils, hämtas det alltid därifrån, oavsett vad som finns på det publika registryt.

SBOM: Software Bill of Materials

SBOM är en maskinläsbar lista över alla komponenter i en mjukvara, inklusive exakta versioner. Formatet har funnits länge men fick nytt liv efter den amerikanska presidentens Executive Order 14028 (maj 2021) som kräver SBOM för mjukvara som säljs till den federala regeringen.

I Europa ställer Cyber Resilience Act (CRA), antagen i oktober 2024, liknande krav. Produkter med digitala element som säljs på EU-marknaden ska ha en SBOM. Kraven träder i kraft stegvis fram till 2027.

För JavaScript-projekt genererar du en SBOM med verktyg som @cyclonedx/cyclonedx-npm eller spdx-sbom-generator. SBOM-filen ger dig och dina kunder en tydlig bild av exakt vilka paket som ingår, vilka versioner de har, och vilka kända sårbarheter som finns.

Upprätta en policy för nya dependencies

Definiera ett enkelt regelverk för när nya dependencies får läggas till:

  1. Finns det ett inbyggt alternativ i Node.js eller webbplattformen?
  2. Har paketet provenance aktiverat?
  3. Hur många maintainers har det? (Ett paket med en enda maintainer är en single point of failure.)
  4. Hur ser commit-historiken ut? Finns det aktiv utveckling eller har paketet inte uppdaterats på flera år?
  5. Hur stort är dependency-trädet? Drar paketet in 5 eller 500 transitive dependencies?
  6. Har paketet postinstall-scripts, och i så fall varför?

Det behöver inte vara en formell process. En checklista i projektets CONTRIBUTING.md eller ett kort meddelande i PR-reviewen räcker.

Tidslinje: Stora supply chain-incidenter i npm och JavaScript

DatumIncidentPåverkan
2017crossenv (typosquatting)Stöld av npm-tokens via miljövariabler
November 2018event-stream/flatmap-streamStöld av privata nycklar riktad mot Copay Bitcoin wallet
Februari 2021Alex Birsan: dependency confusion35 företag drabbade, 130 000+ USD i bug bounties
Oktober 2021ua-parser-js kapningCryptominer och trojaner, 8M nedladdningar/vecka
Januari 2022colors/faker sabotageTusentals projekt gick sönder
Mars 2022node-ipc (protestware)Raderade filer på ryska/belarusiska IP-adresser
Februari 2024Polyfill.io säljs till FunnullDomänbyte, skadlig kod förbereds
Juni 2024Polyfill.io levererar skadlig kod100 000+ sajter drabbade

Slutsats

npm-ekosystemet bygger på förtroende. Varje npm install är ett beslut att lita på att hundratals maintainers, som du inte känner, inte har blivit hackade, inte har blivit utbrända och överlämnat sitt konto till fel person, och inte har bestämt sig för att bevisa en poäng.

Det förtroendet går inte att eliminera helt. Men du kan minska riskytan: använd lockfiles och npm ci, aktivera SRI för CDN-resurser, kör dependency scanning med verktyg som Socket.dev eller Snyk, kontrollera provenance, minimera antalet dependencies, och inför en enkel policy för hur nya paket läggs till.

Polyfill.io-incidenten var inte unik. Den var bara synlig nog att folk reagerade. Nästa incident kan vara ett npm-paket med 50 nedladdningar per vecka som sitter djupt i ditt dependency-träd. Du kommer förmodligen inte märka det förrän det är för sent, om du inte har verktygen och processerna på plats.