tři hodiny axios@1.14.1
31. března 2026 se někdo dostal do npm účtu maintainera axiosu a vydal verzi 1.14.1. Byla venku zhruba tři hodiny, než ji stáhli. Z lidského pohledu to není dlouho. Z pohledu CI je to věčnost, protože každý pipeline na světě, který na čistém checkoutu spustí npm install bez lockfilu a resolvuje caret range typu ^1.14.0, si tu otrávenou verzi klidně stáhl ve chvíli, kdy dopadla do registry. axios je dependency dependency dependency prakticky každého Node projektu, který kdy vznikl, takže blast radius byl i za tři hodiny obrovský.
Samotný payload byl skoro nudný, jak přesně dodržoval osvědčený scénář. Publikovaný tarball deklaroval novou tranzitivní dependency, balíček, o kterém nikdo nikdy neslyšel, který existoval čistě proto, aby nesl postinstall skript. Ten skript stáhl platformově specifickou binárku, cross-platform remote access trojan, který fungoval na Linuxu, macOS i Windows runnerech, a odpálil se hned po dokončení instalace. Žádný exploit, žádná chytrá memory corruption, nic, na co byste psali CVE. Prostě npm dělá přesně to, na co bylo navržené: spouští během instalace libovolný kód od cizích lidí na vašem stroji.
Atribuce padla na Sapphire Sleet, tu samou partu, která už roky jede kampaně typu social engineering vedoucí do supply chainu. Zajímavý na tom nikdy nebyl ten malware. Zajímavé bylo, jak získali práva k publikaci a co všechno údajně vyřešená odpověď oboru na tenhle problém úplně nezvládla.
proč OIDC trusted publishing neudělal vůbec nic
Tady je ta část, která by vám měla zkazit víkend, pokud provozujete něco hodně postaveného na JavaScriptu. Repozitář axiosu měl nakonfigurovaný OIDC Trusted Publishing. GitHub Actions vydává krátkodobý token svázaný s workflow, npm ten token ověřuje proti trusted publisher konfiguraci, žádné dlouhověké credentials se nepovalují nikde, kde by šly ukrást. Přesně to vám každý bezpečnostní článek za poslední dva roky říkal, ať nasadíte, a axios to nasadil. Tady to poskytlo přesně nulovou ochranu.
Nulovou ochranu poskytlo proto, že Trusted Publishing bylo nakonfigurované *vedle* klasického dlouhověkého npm automation tokenu, který nikdo nikdy nezrušil. Účet měl pořád v npm nastavení granular access token, ten typ, co vygenerujete jednou, vlepíte do nějakého secretu a na osmnáct měsíců na něj zapomenete. Sapphire Sleet se GitHub Actions vůbec nedotkli. Nepotřebovali kompromitovat workflow, padělat OIDC claim ani zneužít trust konfiguraci. Přes převzetí účtu získali ten dlouhověký token a spustili npm publish z notebooku, jako by byl rok 2019.
Tohle je foot-gun, o kterém skoro nikdo nemluví. Nastavení Trusted Publishing nevypne vaše existující tokeny. npm je bere jako dvě nezávislé, platné cesty k publikaci a to, že máte tu moderní, tu starou nijak nezavírá. Takže týmy odešly od své migrace s pocitem, že všechno zpevnily, a ve skutečnosti si přidaly druhé vchodové dveře a ty první nechaly dokořán, s tokenem, který nikdy nevyprší a nejspíš má publish scope na každý balíček v celé org.
Běžte to zkontrolovat hned teď. npm token list pro každý účet, který může publikovat cokoli, na čem interně závisíte. Pokud jste migrovali na Trusted Publishing a v tom výpisu je pořád nějaký automation token, máte přesně tenhle problém a žádná OIDC konfigurace na světě vás před ním nezachrání.
koho to zasáhlo a koho ne
Týmy, které z toho vyšly čisté, spadají do dvou skupin a rozdíl mezi nimi vám řekne, na čem doopravdy záleželo. První skupina měla commitnuté lockfily a CI, které místo npm install spouštělo npm ci. npm ci odmítá sahat na package-lock.json, nainstaluje přesně ten resolvnutý strom, který už tam byl, a protože do 1.14.1 nikdo neukazoval, dokud ji vědomě neupdatoval, ty pipeline si celou tři hodiny tahaly 1.14.0, jako by se nic nedělo. Pinování dependencí, přesněji řečeno commitnutí lockfilu a instalace z něj, fungovalo. Jen to říkají všichni a polovina z nás to nedělá v každém pipelinu, obzvlášť ne v těch jednorázových.
Druhá skupina měla lockfily, které *resolvovaly* tu špatnou verzi, protože jejich Dependabot nebo Renovate zrovna během okna otevřel PR, ale stejně je nedostali. Tyhle týmy spouštěly instalace s --ignore-scripts. Škodlivá verze se resolvnula a stáhla, tarball dopadl na disk a postinstall hook se prostě nikdy nespustil. RAT seděl přímo tam v node_modules a nedělal absolutně nic, protože jediné, co potřeboval, tedy aby npm spustilo jeho install skript, bylo globálně vypnuté.
Zbytek dostal hod mincí. Pokud vaše CI během těch tří hodin spustilo holé npm install na čerstvém clonu bez lockfilu, nebo jste rebuildovali Docker vrstvy bez cachování lockfilu, s velkou pravděpodobností jste spustili payload na runneru, který měl přístup k vašim registry credentials, cloud secretům a čemukoli dalšímu, co v build prostředí žije. Což je všechno.
konkrétní změny
Přidejte do CI instalací --ignore-scripts ještě dnes. npm ci --ignore-scripts pro npm, pnpm install --ignore-scripts pro pnpm, a pokud jedete Yarn Berry, nastavte enableScripts: false v .yarnrc.yml. Je to jedna změna s nejvyšší pákou a nestojí vás skoro nic, protože balíčky, které install skripty opravdu potřebují, jsou krátký, známý seznam (nativní moduly jako better-sqlite3, sharp, nějaké interní věci Prismy) a přesně ty si dáte na allow-list přes onlyBuiltDependencies v pnpm nebo přes scoped rebuild krok pro tu hrstku, co to potřebuje. Přes devadesát procent vašeho stromu dependencí nemá při instalaci co spouštět kód, tak mu to přestaňte dovolovat.
Další na řadě je audit toho dvojitého auth vzoru. U každé npm org, pod kterou publikujete, vypište aktivní tokeny a zrušte každý dlouhověký automation token, který mělo Trusted Publishing nahradit. Pokud jste migrovali na OIDC, migrace není hotová, dokud ten starý token nezmizí, tečka. Stejná logika platí pro PyPI trusted publishing a pro každou registry, kde jste na starý flow přišroubovali moderní, ale ten legacy path jste nesmazali.
Udělejte z npm ci povinnost. Pokud kdekoli ve vaší org spouští nějaký pipeline npm install na build agentovi, je to bug. Commitujte lockfily, nechte build spadnout, když je lockfile out of sync, a nikdy nedovolte CI resolvovat čerstvé ranges proti živé registry, protože přesně tímhle oknem tenhle útok projel jak náklaďák.
A oddělte update flow od build flow. Bumpy dependencí by měly přistát ve větvi, buildnout se s vypnutými skripty, projít scanem a mergnout se teprve poté, co se člověk nebo policy podívá, co se v resolvnutém stromu doopravdy změnilo. Tříhodinovou verzi automaticky mergnul příliš horlivý bot, a přesně tak pár týmů proměnilo těsný únik v incident.
co jsme změnili na vlastních pipelinech
V steezru stavíme spoustu Next.js aplikací a Node backendů, a tenhle případ nás donutil zauditovat vlastní barák dřív, než jsme klientům řekli jediné slovo. Každá CI instalace napříč našimi repozitáři teď defaultně běží s vypnutými skripty, s malým per-project allow-listem pro ty dva tři balíčky, které nativní build legitimně potřebují, a ten allow-list žije v repu, kde ho reviewer vidí v diffech. Našli jsme jeden interní tooling balíček se starým npm automation tokenem z nastavení v roce 2024, které předcházelo našemu přechodu na Trusted Publishing, přesně ten vzor, co dostal axios, a to odpoledne jsme ho zabili.
Déle trvala ta část kolem Dockeru. Spousta našich build imagů dělala COPY package.json a pak npm install, což uvnitř kontejneru úplně zahodí záruku lockfilu, i když má hostitelský repo perfektní lockfile. Přepsali jsme každý Dockerfile tak, aby kopíroval package.json i package-lock.json a spouštěl npm ci --ignore-scripts, což jako bonus dramaticky zlepšilo i naše cachování vrstev.
Nic z toho není exotické. Je to nudná hygiena, na kterou při postmortemu všichni pokyvují hlavou a pak ji potichu odsunou, protože pipeline funguje a shipování feature připadá naléhavější. Axios je čistý, čerstvý, reálný příklad, který můžete položit vedení na stůl, když potřebujete rozpočet na týden nezáživného CI úklidu. Tři hodiny, jeden dlouhověký token, na který si nikdo nepamatoval, jeden postinstall hook a RAT na runnerech, které mohly číst každý secret, co máte. To je celý příběh a ty opravy jsou den práce, který ještě neděláte.
