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 omdirigerade 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.
Ingen server hackades: domänen hade bytt ägare. Domänen och det tillhörande GitHub-kontot hade sålts av den dåvarande ägaren till det kinesiska företaget Funnull. Projektets skapare, Andrew Betts (tidigare utvecklare på Financial Times), hade aldrig ägt domänen. 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 ett paket 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, som aldrig ägt domänen, varnade på X: "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 avancerad. 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 hade redan i februari lanserat 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-hostad 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. - electorn (istället för electron): Samlade in IP-adresser, geolokalisering och enhetsdata.
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.
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 lyckades köra kod hos över 35 organisationer, bland dem Apple, Microsoft, PayPal, Shopify, Netflix och Yelp. 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 har dokumenterat att AI-modeller ofta producerar samma felaktiga paketnamn, vilket gör dem förutsägbara nog att utnyttja. En studie presenterad på USENIX Security 2025 visade att kommersiella modeller föreslog icke-existerande paket i runt 5,2 % av fallen och öppna modeller i runt 21,7 %.
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
På npm-registret kan ett publicerat versionsnummer inte ersättas, så en pinnad version skyddar mot nya skadliga versioner. Lockfilen skyddar däremot inte om den pinnade versionen var skadlig redan när du lade till den, och integritets-hashen fångar manipulation på vägen om du använder npm ci. En lockfile hjälper alltså inte mot attacker som sker vid initial install, 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 avsiktligt. 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.
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. För statiska filer förhindrar det att ändrad kod körs.
Begränsningen med SRI är att det inte fungerar för dynamiska resurser. Polyfill.io var svårare att skydda så, eftersom svaret varierade med User-Agent och det därför inte går att ange 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, och långt ifrån alla populära paket har det aktiverat.
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?
fetchfinns inbyggt i Node.js 18+.cryptofinns inbyggt.path,fs,urlfinns inbyggt. - Är det här paketet värt sin dependency-kedja? Paketet
is-odd(som checkar om ett tal är udda) har runt 350 000 nedladdningar per vecka och drar in ett eget beroende (is-number) för en kontroll som i princip ärMath.abs(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.getseparat, 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:
- Finns det ett inbyggt alternativ i Node.js eller webbplattformen?
- Har paketet provenance aktiverat?
- Hur många maintainers har det? (Ett paket med en enda maintainer är en single point of failure.)
- Hur ser commit-historiken ut? Finns det aktiv utveckling eller har paketet inte uppdaterats på flera år?
- Hur stort är dependency-trädet? Drar paketet in 5 eller 500 transitive dependencies?
- 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.
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 riskytan går att minska:
- använd lockfiles och
npm ci - använd SRI för CDN-resurser
- kör dependency scanning med verktyg som Socket.dev eller Snyk
- kontrollera provenance
- minimera antalet dependencies
- inför en enkel policy för hur nya paket läggs till
Nästa incident kan lika gärna gälla ett npm-paket med 50 nedladdningar per vecka som sitter djupt i ditt dependency-träd.
Källor
- Sansec: Polyfill supply chain attack hits 100K+ sites
- Cloudflare: Polyfill.io now available on cdnjs
- Simon Willison: Polyfill supply chain attack
- GitHub Advisory Database: GHSA-pjwm-rvh2-c87w (ua-parser-js)
- CISA: Malware Discovered in Popular NPM Package, ua-parser-js
- GitHub Blog: Introducing npm package provenance
- npm Docs: npm Unpublish Policy