2024 júniusában több mint 100 000 weboldal futtatott rosszindulatú kódot anélkül, hogy egyetlen sort is megváltoztattak volna a saját forráskódjukban. A polyfill.io CDN-t egy kínai cég vásárolta fel, majd a kiszolgált JavaScript fájlba mobil felhasználókat átirányító kódot csempészett – azok az oldalak pedig, amelyek egyszerűen csak beillesztették a <script src="https://cdn.polyfill.io/..."> tagot, gyakorlatilag aláírás nélküli csekket adtak egy harmadik félnek (Sansec elemzése). Van egy egyszerű, régóta létező böngészőfunkció, ami pont ez ellen védekezik, mégis a legtöbb projektben nincs bekapcsolva: a Subresource Integrity.
Nézzük meg, hogyan működik, miért nem terjedt el jobban, és hol vannak a korlátai.
Mi történt a polyfill.io-val
A polyfill.io egy népszerű szolgáltatás volt, ami a böngésző User-Agent fejléce alapján generált polyfill JS-t régebbi böngészőkhöz. 2024 februárjában egy Funnull nevű cég megvásárolta a domaint és a hozzá tartozó GitHub szervezetet is. Négy hónappal később, június 25-én a Sansec biztonsági kutatói azt találták, hogy a szolgáltatás mobil eszközökön futó látogatókat egy hamis Google Analytics domainen (www.googie-anaiytics.com) keresztül sportfogadási és egyéb oldalakra irányított át – adminisztrátori fiókoknál és analitikai szolgáltatások jelenlétekor pedig kikapcsolta magát, hogy nehezebb legyen észrevenni.
A Cloudflare és a Fastly saját, változatlan tükröt (mirror) hozott létre a régi, még biztonságos kódból, és a Cloudflare az ingyenes csomagon lévő oldalakon automatikusan átírta a polyfill.io-ra mutató linkeket a saját tükrükre – ezt írták le a Cloudflare blogbejegyzésében. Ez gyors és hatékony mentőöv volt, de nem old meg semmit hosszú távon: a probléma az volt, hogy a beágyazó oldalak semmilyen módon nem ellenőrizték, hogy a betöltött script tartalma megegyezik-e azzal, amit eredetileg jóváhagytak.
Amit a <script src> alapból nem csinál meg
Amikor beillesztesz egy külső scriptet, a böngésző megbízik abban, hogy amit a megadott URL-ről kap, az az, amire számítasz. Ez a bizalom önmagában nem rossz – enélkül nem működne a web –, de tranzitív: nem csak a CDN üzemeltetőjében kell megbíznod, hanem mindenkiben, aki valaha hozzáférést szerezhet ahhoz a domainhez, DNS-hez vagy tárolt fájlhoz. Ez lehet egy feltört admin fiók, egy elfelejtett domain-megújítás, vagy – mint a polyfill.io esetében – egy sima tulajdonosváltás.
A Subresource Integrity (SRI) specifikáció pontosan ezt a hiányzó ellenőrzést pótolja: megadhatsz egy kriptográfiai hash-t a betöltendő fájlhoz, és a böngésző csak akkor futtatja le vagy alkalmazza a betöltött tartalmat, ha annak hash-e egyezik a megadottal.
Hogyan működik gyakorlatban
Az integrity attribútum <script> elemeken, illetve stylesheet, preload vagy modulepreload típusú <link> elemeken használható (MDN – Subresource Integrity). Az értéke egy algoritmus-base64hash formátumú string, ahol az algoritmus sha256, sha384 vagy sha512 lehet:
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"></script>
A hash-t magad generálod, mielőtt beillesztenéd a linket – tehát azt a verziót „fagyasztod be”, amit ellenőriztél. Terminálban ez két sor:
curl -s https://cdn.example.com/lib.js -o lib.js
openssl dgst -sha384 -binary lib.js | openssl base64 -A
Ha nincs kéznél a terminál, a srihash.org generátor URL alapján is elkészíti a hash-t. A crossorigin="anonymous" attribútum nem opcionális: az SRI-hez a böngészőnek CORS módban kell letöltenie az erőforrást, mert a `no-cors` módban a válasz tartalmát a JavaScript nem is olvashatná el – ha engedélyeznék az integrity-ellenőrzést ilyenkor is, azzal egy támadó próba-szerencse hash-ekkel következtethetne a védett tartalomra. Emiatt a szervernek küldenie kell egy megfelelő Access-Control-Allow-Origin fejlécet is, különben a böngésző elutasítja a kérést.
Ha a letöltött fájl akár egyetlen byte-ban is eltér a megadott hash-től, a böngésző hálózati hibaként kezeli a kérést, és a script nem fut le. Ezt könnyű demonstrálni: egy plusz szóköz a fájl végén is teljesen más hash-t ad:
$ node -e "
const crypto = require('crypto');
const h = s => crypto.createHash('sha384').update(s).digest('base64');
console.log(h('console.log(\"hello\");'));
console.log(h('console.log(\"hello\"); '));
"
ym/3cF/gAcW1/eerNQGFaOSOuU2LHI6RYvChaVur/G+PkVozZ5Zo8Uu5R2S2zm7C
8a6xKZRMHMSBz5kzqHGxCM0pt1sfpG9JwOXhBcC5rrlM+mlJ317kUI2cSU2juh6E
Ha a polyfill.io szolgáltatás felhasználói SRI-vel töltötték volna be a scriptet, a hash egyszerűen nem egyezett volna a manipulált tartalommal, és a böngésző visszautasítja a betöltést – a látogató legfeljebb egy hiányzó polyfill miatti funkcionális hibát látott volna, nem pedig átirányítást egy fogadóoldalra.
Miért nem használja mindenki
Az SRI ára az, hogy pontosan azt a rugalmasságot veszed el, amiért sokan CDN-t használnak. Ha a szolgáltató bármikor lecseréli a fájl tartalmát ugyanazon az URL-en – akár csak egy bugfix miatt, akár mert a build pipeline-jában valami minifikálási beállítás változik –, a hash-ed elévül, és az oldalad eltörik, amíg nem frissíted az attribútumot. Emiatt SRI-t gyakorlatilag csak verziózott, változatlan URL-eken érdemes használni (pl. [email protected]/dist/lib.min.js), soha nem @latest vagy hasonló mutable végponton.
A másik korlát, hogy az integrity attribútum csak a fent említett HTML elemekre vonatkozik. Ha dinamikusan, fetch()-csel vagy import()-tal töltesz be kódot, az attribútum nem véd – bár a fetch() API-nak van saját integrity opciója ugyanezzel a hash-formátummal, amit külön be kell állítani. Sok modern build eszköz (bundlerek, npm-csomagok) egyáltalán nem generál SRI hash-eket automatikusan a kimenethez, úgyhogy ha valaki self-hosted third-party scripteket használ CDN helyett, könnyen elfelejtkezik róla.
És persze SRI csak azt védi, amit explicit módon hash-eltél. Ha a támadó nem a CDN-en lévő fájlt módosítja, hanem az oldaladba beillesztett, harmadik féltől származó inline scriptet (pl. egy feltört tag manager konfiguráción keresztül), az SRI nem old meg semmit – ott a Content Security Policy script-src és nonce-alapú szabályai jelentenek védelmet, ami más réteg, más probléma ellen.
Van egy újabb réteg is: Integrity-Policy fejléc
Az integrity attribútum önmagában opt-in: ha egy fejlesztő elfelejti kitenni, semmi sem figyelmezteti rá. Erre válasz a Integrity-Policy HTTP fejléc, amivel a szerver megkövetelheti, hogy bizonyos erőforrástípusok (pl. scriptek) csak integrity-ellenőrzéssel töltődhessenek be – enélkül a böngésző blokkolja a kérést. Ez a fejléc jelenleg még nem Baseline funkció, tehát nem minden elterjedt böngésző támogatja, úgyhogy éles bevezetés előtt mindenképp nézd meg az aktuális támogatottságát az MDN kompatibilitási táblázatában, mielőtt bármire is építenél rá.
Mikor éri meg
Nem kell minden npm-ből importált csomaghoz SRI-t generálni – ott a package-lock.json és a registry saját integritás-ellenőrzése (ami szintén hash-alapú) már véd. SRI ott ér a legtöbbet, ahol a kódod olyan URL-ről tölt be scriptet vagy stylesheetet, ami felett nincs teljes kontrollod: nyilvános CDN-ek (cdnjs, jsDelivr, unpkg), harmadik féltől kapott widgetek, fizetési vagy analitikai SDK-k. Ha ilyet használsz, és a szolgáltató verziózott, változatlan URL-t ad, a hash hozzáadása néhány perc, cserébe pontosan az a fajta csendes, észrevétlen kompromittálódás ellen védekezel, ami a polyfill.io esetében több tízezer oldalt érintett anélkül, hogy bárki egyetlen sort is módosított volna a saját kódjában.