Az innerHTML mostantól típusellenőrzött lehet: a Trusted Types API bekerült minden nagy böngészőbe

A CSP a forrásokat szűri, de nem véd a saját kódod innerHTML-hívásai ellen. A Trusted Types API típust ad a stringeknek a DOM veszélyes pontjain – és 2026-ra a Firefox és a Safari is felzárkózott, így most már mindhárom nagy böngészőmotor kikényszeríti.

A CSP-t (Content Security Policy) sokan úgy kezelik, mint ami „megoldja” az XSS-t. Csakhogy a klasszikus CSP a forrásokat engedélyezi vagy tiltja (honnan tölthető be script, stílus, kép), és nem véd az ellen, hogy a saját kódod egy nem megbízható stringet egyszerűen beírjon a DOM-ba egy innerHTML = vagy eval() hívással. Ez a DOM-alapú XSS, és ez az a kategória, amit a legtöbb csapat a legkevésbé kontrollál, mert nem a szerver oldalán, hanem a kliens JavaScript belsejében történik. A Trusted Types API pontosan erre való – és 2026 elején végre mindhárom nagy böngészőmotor eljutott odáig, hogy támogassa.

Diagram: egy nem megbízható string a Trusted Types szabályzaton (policy) áthaladva TrustedHTML objektummá alakul, mielőtt eléri az innerHTML sinket, míg a szabályzat nélküli string a require-trusted-types-for CSP direktívánál TypeError-ral elakad

A probléma: a string és a „biztonságos string” ugyanaz a típus

A DOM-nak van néhány API-ja, amit a spec és a böngészőgyártók „injection sinkként” tartanak számon: ilyen a innerHTML, outerHTML, document.write() a HTML oldalon, az eval() és a Function() konstruktor a JavaScript oldalon, illetve a script.src és a Worker() konstruktor az URL oldalon. Ezek mindegyike simán elfogad egy sima JavaScript stringet – és a nyelvben semmi nem különbözteti meg a "<b>szia</b>" irodalmi konstansot attól a stringtől, ami közvetlenül a felhasználó profiljának bemutatkozó szövegéből jött. A típusrendszer szemszögéből mindkettő ugyanaz: string.

Ebből fakad, hogy egy nagy kódbázisban gyakorlatilag lehetetlen manuálisan garantálni, hogy minden egyes innerHTML-hozzárendelés előtt tényleg lefutott a sanitizálás. Elég egy új komponens, egy refaktor vagy egy harmadik féltől származó widget, ami kihagyja a lépést, és megvan a DOM XSS. A Trusted Types ezt oldja meg úgy, hogy bevezet egy új típust a stringek fölé, amit a böngérző maga kényszerít ki a sinkeknél.

Hogyan működik: policy, TrustedHTML, és egy CSP-kapcsoló

A mechanizmus három részből áll. Először létrehozol egy policyt a trustedTypes.createPolicy() hívással, ami megmondja, hogyan alakuljon egy sima stringből TrustedHTML, TrustedScript vagy TrustedScriptURL objektum:

const htmlPolicy = trustedTypes.createPolicy("app-html", {
  createHTML: (input) =>
    input.replace(/[&<>"]/g, (c) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])
    ),
});

// A policy objektumon keresztül kapott érték már TrustedHTML,
// nem sima string – ezt fogadja el a böngésző az innerHTML-nél.
const safe = htmlPolicy.createHTML(userComment);
container.innerHTML = safe;

Ha ezt lefuttattam Node-ban (természetesen a trustedTypes globális nélkül, csak magát az escape-elő logikát), a kimenet pontosan az elvárt: a <img src=x onerror=alert(document.cookie)> bemenetből &lt;img src=x onerror=alert(document.cookie)&gt; lesz – ártalmatlan szöveg, nem futó kód. A Trusted Types API maga nem sanitizál semmit, ez csak egy egyszerű escape példa; élesben inkább egy tesztelt könyvtárra, például a DOMPurify-ra érdemes bízni, ami kifejezetten támogatja is ezt: DOMPurify.sanitize(dirty, { RETURN_TRUSTED_TYPE: true }) már közvetlenül TrustedHTML-t ad vissza.

A második rész a kikényszerítés: a require-trusted-types-for 'script' CSP direktíva bekapcsolása után a böngésző TypeError-t dob, ha bárhol egy sima stringet próbálsz beírni egy sinkbe policy nélkül – tehát nem opcionális védőháló lesz, hanem a futásidő megtagadja a műveletet:

Content-Security-Policy:
  require-trusted-types-for 'script';
  trusted-types app-html default;

A harmadik rész a trusted-types direktíva, ami felsorolja, mely policynevek hozhatók létre egyáltalán – ha egy beágyazott script vagy egy kompromittált függőség saját policyt próbálna csinálni más néven, azt a böngésző elutasítja. Ez azért fontos, mert egy támadó, aki már el tudott helyezni valamennyi kódot, simán létrehozhatna egy „álságos” policyt, ami mindent átenged.

A „default” policy: mentőöv a legacy kódnak

A gyakorlatban ritkán lehet egy meglévő alkalmazásban minden sinket egyszerre lecserélni policy-hívásra. Erre való a "default" néven regisztrált policy: ha a kikényszerítés be van kapcsolva, és egy kód sima stringet ad át egy sinknek anélkül, hogy explicit policyt hívott volna, a böngésző automatikusan lefuttatja rajta a default policyt, mielőtt eldobná a hívást. Ez tökéletes átmeneti eszköz: a default policyba betehetsz egy naplózást („ide még nem migráltunk”), miközben biztonságosan sanitizálja is a bemenetet, hogy a termék közben ne törjön el.

A böngészőtámogatás: ez most vált tényleg univerzálissá

A Chrome (és vele az Edge) 2020 óta, a 83-as verziótól kezdve támogatja a Trusted Types-t – innen ered, hogy elsőre „Google-témának” tűnhet. A hosszú ideig hiányzó két nagy motor viszont az utóbbi hónapokban zárkózott fel: a Firefox a böngészőtámogatási táblázat szerint a 148-as verziótól (2026. február) adja, a Safari pedig a 26-os verziótól. Ez azt jelenti, hogy amíg tavaly egy `require-trusted-types-for`-ral védett oldal a nem-Chromium böngészőkben egyszerűen nem kapott védelmet (a direktívát ismeretlenként figyelmen kívül hagyták), mostantól a három fő motoron már ténylegesen érvényesül a szabály. A WebKit saját hibakövetőjében ugyanakkor még akadnak nyitott tételek – például a Workerekben futó Trusted Types jelentések még nem teljesek –, szóval ha worker-scriptekben is támaszkodnál rá, érdemes külön letesztelni.

Korlátok, amikre érdemes számítani

  • Nem ad sanitizálást „ingyen” – a policy függvényeit neked (vagy egy könyvtárnak, mint a DOMPurify) kell megírni. Egy rosszul megírt createHTML, ami csak visszaadja a bemenetet változtatás nélkül, formálisan megfelel az API-nak, de semmit nem véd.
  • Bevezetéskor szinte biztos, hogy lesz olyan harmadik féltől származó script (analitika, widget, régi jQuery-plugin), ami közvetlenül ír a sinkekbe – ezeket vagy default policyval kell lefedni, vagy kompatibilis alternatívát keresni.
  • A Content-Security-Policy-Report-Only fejléc is támogatja a direktívát, úgyhogy élesítés előtt érdemes report-only módban futtatni, és a beérkező jelentésekből összeszedni, mely sinkek törnének el.
  • A védelem csak azokra a sinkekre terjed ki, amiket a spec definiál – egy saját, kézzel írt eval-szerű mechanizmus (pl. dinamikus new Function string-összefűzésből) kívül eshet rajta, ha nem a natív sinket használod.

A Trusted Types tehát nem helyettesíti a CSP forrás-alapú szabályait, és nem old meg mindent automatikusan – de olyan hibaosztályt zár ki futásidőben, amit sem code review-val, sem linterrel nem lehet garantáltan kiszűrni egy nagyobb kódbázisban. Ha van olyan alkalmazásod, ahol sok helyen kerül felhasználói vagy harmadik féltől származó adat közvetlenül a DOM-ba, most már minden fő böngészőben érdemes komolyan megfontolni a bevezetését – kezdve egy report-only CSP-vel, hogy lásd, hol fájna.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük