A CSP ne domaineket bízzon meg: nonce és strict-dynamic az XSS ellen

A nonce-alapú, strict-dynamic CSP nem hostlistákat, hanem az induló, valóban megbízható JavaScriptet jelöli ki. Így vezesd be úgy, hogy a cache és a régebbi kliensek se legyenek vakfoltok.

Egy CSP-fejlécet könnyű úgy kipipálni, hogy script-src 'self', aztán kész. Ettől még az XSS elleni védelem gyakran csak látszólagos: a saját originről betölthető régi JavaScript, egy engedélyezett külső szolgáltató vagy egy rosszul karbantartott hostlista könnyen túl nagy bizalmi kört ad. A Content Security Policy akkor ad érdemi második védelmi vonalat, ha nem URL-eket, hanem konkrétan megbízható kódrészleteket jelölünk ki.

Nonce-alapú Content Security Policy folyamat: a szerver csak az ellenőrzött szkripteket engedi futni

Erre való a nonce-os, 'strict-dynamic'-os CSP. Nem XSS-helyettesítő: a bemenetek helyes kódolását és a biztonságos DOM-kezelést továbbra is az OWASP XSS-ajánlásai szerint kell megoldani. Viszont egy hibán át becsúszó <script> futását a böngésző akkor is megállíthatja.

A nonce nem egy titkos konfigurációs érték

A szerver minden HTML-válaszhoz készít egy kriptográfiailag véletlen, egyszer használatos értéket. Ugyanez bekerül a Content-Security-Policy fejlécbe és azoknak a script elemeknek a nonce attribútumába, amelyeket a szerver szándékosan kiküldött. A böngésző csak az egyezést fogadja el. A script-src dokumentációja külön kiemeli, hogy a nonce-nak válaszonként egyedinek kell lennie.

import { randomBytes } from "node:crypto";

function createCsp() {
  const nonce = randomBytes(16).toString("base64");
  const policy = [
    "default-src 'self'",
    `script-src 'nonce-${nonce}' 'strict-dynamic' 'self'`,
    "object-src 'none'",
    "base-uri 'none'",
  ].join("; ");

  return { nonce, policy };
}

Az alkalmazás a policy értékét HTTP-fejlécként küldi, majd a sablonban ugyanazt a nonce-ot használja: <script nonce="…" src="/assets/boot.js" defer></script>. A nonce ne legyen buildid, sessionazonosító vagy állandó környezeti változó. Ha egy támadó egy előző válaszból ismeri, a policy már nem választja el a saját scriptjét a szerverétől.

Mit ad hozzá a strict-dynamic?

Nonce önmagában azt mondja meg, melyik kezdeti script indulhat el. A CSP3 'strict-dynamic' szabálya ehhez bizalmi láncot tesz: egy nonce-os vagy hash-elt, ezért megbízhatónak kijelölt script által programból létrehozott script elem is betölthető. Tipikus eset a bootstrap fájl, amely a route alapján tölti a kóddarabokat vagy egy harmadik féltől származó SDK-t.

Ez nem azt jelenti, hogy „minden dinamikus script mehet”. A bizalom a jóváhagyott kezdő scriptből ered. Egy HTML-injekcióval beszúrt, nonce nélküli, parser által feldolgozott <script src="…"> továbbra is blokkolódik. Ezzel a hostlista állandó gondozása helyett arra a jóval rövidebb listára koncentrálhatunk, melyik belépő kódot adjuk ki a kezünkből.

A nonce nem engedélyezi az inline eseménykezelőket sem: az onclick="…" és társai számára nem helyettesíti az 'unsafe-inline'-t. Ez jó kényszer. A handler kerüljön a nonce-os külső vagy inline scriptbe addEventListener-rel. Az 'unsafe-inline' felvétele csak azért, hogy a régi sablon működjön, a régebbi CSP-értelmezőkben épp azt a védelmet oldhatja fel, amelyet bevezetnénk. Ugyanezért az 'unsafe-eval' engedélyezése is legyen külön, megindokolt kivétel.

A példában szereplő 'self' nem véletlen. A strict CSP telepítéséről szóló web.dev útmutató szerint CSP3-as böngészőben a nonce és a 'strict-dynamic' mellett a hostforrások nem adják a tényleges engedélyt; a régebbi CSP-értelmezők viszont nem ismerik a 'strict-dynamic'-ot, ezért náluk a 'self' marad visszaesési szabály. Ha a bootstrap külső originről tölt további scriptet, a régebbi kliensekhez annak célzott hostját is fel kell venni – nem egy általános https: engedélyt.

A bevezetés buktatója: a cache

A nonce-os oldal dinamikus HTML. Egy közös CDN- vagy reverse-proxy-cache nem szolgálhatja ki változatlanul ugyanazt a HTML-t és CSP-fejlécet sok kérésre, mert ezzel a nonce újrahasználódik. Ilyenkor vagy az edge-en kell válaszonként beilleszteni az értéket, vagy a stabil inline kódhoz hash-alapú CSP-t érdemes választani. A kettőt keverni is lehet, de a nonce-nak akkor is frissnek kell maradnia.

WordPress-, tag manager- és widgetdús oldalon a migráció munka: minden engedélyezett inline scriptnek meg kell kapnia a nonce-ot, a beillesztett külső kódot pedig fel kell mérni. Első körben célszerű ugyanazzal a policyval Content-Security-Policy-Report-Only fejlécet küldeni, figyelni a sértéseket, majd a szükséges eseteket megszüntetni vagy tudatosan engedélyezni. A cél nem a nullára csökkentett konzolhiba, hanem az, hogy ne maradjon indokolatlan scriptfuttatási út.

Mikor éri meg?

Ott különösen hasznos, ahol felhasználói vagy CMS-ből származó tartalom megjelenik, sok kliensoldali kód fut, és egy XSS ára magas. Nem varázspajzs: egy nonce-os, már futó saját kódban lévő DOM-XSS-t nem javít ki. Viszont szűkíti a hibák kihasználható felületét, és egy jó boot.js-t valódi bizalmi gyökérré tesz. A fontos váltás fejben ennyi: ne azt kérdezd, „melyik domain tölthet scriptet?”, hanem azt, „melyik konkrét kód indulhat el először?”

Források

Leave a Reply

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