
← Terug naar blog
Hoe XSS de DOM bereikt, welke browser APIs sinks zijn en mitigaties die in productie werken—sanitisatie, CSP, cookies en CSRF pairing.
Als een string wordt geïnterpreteerd als markup of script in plaats van inert text, dan wordt die inhoud uitgevoerd als code in de oorsprong van je pagina—met dezelfde privileges als je bundel. Dat is XSS: geen styling bug, maar een verwarde deputy tussen data en instructies. Dit artikel is de technische ruggraat—aanvalmechanica, sinks, fixes die schalen, plus een uitvoerbare playgrounds (Vite + React op main; live: playgrounds.lucascoliveira.com) en verwijzingen naar deze Next.js repo waar headers en MDX het standaard risico verminderen.
Scripts die in je pagina zijn geïnjecteerd, worden uitgevoerd met dezelfde oorsprong als je bundel. In de praktijk betekent dit vaak:
document.cookie zichtbaarheid voor cookies die niet zijn gemarkeerd als HttpOnly (sessie-diefstal / fixatie chaining).fetch() / XMLHttpRequest naar je API met omgevingsreferenties als cookies worden verzonden en CORS het toestaat—of exfiltratie van tokens die zijn opgeslagen in localStorage / sessionStorage als je app ze daar plaatst.Dus de mitigatie is niet "gebruik React"—het is nooit het toewijzen van door de aanvaller gecontroleerde bytes aan een sink die HTML parseert of script uitvoert, tenzij een beoordeelde pijplijn ze heeft gereduceerd tot een veilig type.
Opgeslagen XSS — Niet-vertrouwde HTML (of een payload die HTML wordt na sjabloonuitbreiding) wordt persistent gemaakt (DB, cache, zoekindex). Elke gebruiker die dat record laadt, raakt de sink. Frontend-impact: de eerste weergave die innerHTML = row.body of equivalent uitvoert zonder sanitisatie, voert de payload uit.
Gereflecteerde XSS — De payload blijft nooit persistent; het stuitert terug van de server of routinglaag in het antwoord. Klassiek: ?q=<script>…</script> gereflecteerd in de HTML zonder codering. SPA-equivalent: location.search of hash parsed client-side en geschreven in de DOM zonder codering. De oplossing is hetzelfde: behandel door URL afgeleide strings als data; als je ze moet reflecteren, codeer ze voor de context (zie hieronder).
DOM-gebaseerde XSS — Het serverantwoord is "schoon", maar client-side script leest door de aanvaller gecontroleerde invoer (location, referrer, postMessage, WebSocket-berichten) en geeft het door aan een sink. Voorbeeld: eval("handle" + location.hash.slice(1)) of element.innerHTML = decodeURIComponent(...). Statische analyse van sjablonen is niet genoeg; je moet elke pad van niet-vertrouwde invoer naar sink controleren.
Dit zijn de gebruikelijke schuldigen in React/SPA-codebases:
| Sink | Risico |
|---|---|
element.innerHTML, insertAdjacentHTML | Parseert HTML; elke tag/gebeurtenishandler die je toestaat, kan script uitvoeren. |
dangerouslySetInnerHTML | Dezelfde als boven—React sanitiseert niet. |
document.write | Dezelfde. |
eval, new Function, setTimeout(string) | Directe scriptuitvoering. |
javascript: URLs in href / src | Navigatie of bronbelasting die wordt uitgevoerd als script-URL. |
postMessage handlers die eval of HTML instellen vanuit event.data | XSS als origin niet wordt gevalideerd of event.data een sink bereikt zonder een veilig contract—niet alleen "verkeerd venster" fouten. |
Geen sink per default: textContent, createTextNode, React's normale kindtekst, attributen die React behandelt als strings als je ze niet omzeilt. Markdown-pijplijnen worden sinks als ze onbewerkte HTML produceren en je die HTML aan de DOM toewijst zonder te sanitizeren.
Demo-opmerking (belangrijk): In HTML5 worden <script>-knooppunten die zijn ingevoegd via innerHTML / dangerouslySetInnerHTML niet uitgevoerd—de parser voert ze niet uit zoals een klassieke gereflecteerde XSS. Om uitvoering te zien als HTML wordt geïnjecteerd, vertrouwen payloads meestal op attribuut handlers (bijv. onerror op img) of vergelijkbaar. De XSS-playground houdt payloads vooraf ingesteld (zie payloadPresets.ts) zodat je testgevallen kunt testen die daadwerkelijk afgaan na invoering.
Als de UI alleen platte tekst nodig heeft — Bind tekst met textContent, React-tekstkinderen of MDX die compileert naar componenten zonder een HTML-pijplijn. Geen sanitizer vereist; je bent niet in het HTML-spel.
Als je rijkere tekst nodig hebt (bold, lijsten, koppelingen) — Je hebt ofwel een beperkte opmaaktaal die is samengesteld tot veilige elementen of HTML- sanitisatie met een allowlist (tags + attributen). Codering (bijv. HTML-entiteit escaping) is voor het plaatsen van data in HTML-tekstknooppunten; sanitisatie is voor wanneer je een subset van HTML moet toestaan. Verwissel de twee niet.
Defensie in diepte voor rijkere tekst in echte producten — Valideer/sanitize bij het schrijven (API weigert onbekende tags, lengte limieten) en sanitize of render door een veilige pad bij het lezen (weergavelaag). Opslag kan worden teruggedraaid, gecorrumpeerd of geschreven door een andere servicerversie.
DOMPurify is een browser-sanitizer met een standaardprofiel; je configureert het nog steeds voor je product:
ALLOWED_TAGS / ALLOWED_ATTR — Begin minimaal (p, br, strong, em, a met href alleen als je koppelingen nodig hebt). Elke extra tag is een aanvaloppervlak.ADD_ATTR / FORBID_TAGS — Expliciet verslaat "bijna alles toestaan".RETURN_DOM / RETURN_TRUSTED_TYPE — Geef de voorkeur aan DOM-knooppunten of TrustedHTML-achtige uitvoer als je integreert met Trusted Types.afterSanitizeAttributes — Strip href-waarden die beginnen met javascript: of vreemde data: MIME-types als je koppelingen toestaat.In playgrounds op main, contrasteert de XSS-playground een onveilige weergavepad met een gesaniseerde pad (DOMPurify-allowlist)—dezelfde UI, verschillend betrouwbaarheidsbeleid.
CSP vermindert wat kan worden uitgevoerd als iets erdoorheen glipt. In apps/web/next.config.ts stelt deze site een CSP in met default-src 'self', strakke object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'none', plus script-src / style-src met 'unsafe-inline' omdat Next.js App Router + MUI sx momenteel inline script/style nodig hebben in deze installatie—gedocumenteerd in code. Nonce- of hash-gebaseerde script-src zou breed inline script toestaan verwijderen maar vereist middleware om nonces per verzoek in te injecteren—waard om te plannen als je zelden gebruikers-HTML verzendt.
Realiteitscheck: CSP vervangt geen sanitisatie voor gebruikers-HTML; het vernauwt de explosieradius (bijv. kan script-hosts blokkeren die je niet hebt toegestaan). Inline gebeurtenishandlers (onerror, enz.) worden niet automatisch geneutraliseerd alleen omdat je een CSP instelt—'unsafe-inline' op script-src is gebruikelijk in echte apps (inclusief de Next/MUI-installatie van deze site), en blokkeren van handlers vereist meestal expliciete script-src / script-src-attr (of nonces/hashes), afhankelijk van browser en CSP-niveau.
XSS kan CSRF-tokens omzeilen als de token leesbaar is vanuit de DOM of als het aanvaller-script verzoeken doet met referenties. Dus: geef prioriteit aan XSS-oplossingen; ook:
HttpOnly, Secure, SameSite=Lax of Strict waar flows het toestaat—vermindert cross-site cookie-lekkage en klassieke CSRF.SameSite-cookies, anti-CSRF-tokens of aangepaste headers + CORS-beleid zodat willekeurige sites geen POST-verzoeken met referenties kunnen doen.Frontend-taak: doe geen geheimen in JS-leesbare opslag als het te vermijden is; gebruik fetch met expliciet credentials-beleid dat is afgestemd op je API-ontwerp.
apps/web/next.config.ts: beveiligingsheaders op /(.*); CSP-tekenreeks gebouwd in code met env-specifieke script-src (dev unsafe-eval voor React-stacks alleen waar nodig).apps/web/app/api/chat/route.ts: JSON-parsing, lege controle, MAX_MESSAGE_LENGTH-kap—misbruik vormgeven, geen XSS op zichzelf.apps/web/lib/blog/mdx.tsx: MDX met een vaste componentkaart (next-mdx-remote/rsc), geen onbewerkte HTML-tekenreeksen uit CMS. Verschillend dreigingsmodel dan "berichttekst met HTML".main je geeftKloon playgrounds, voer npm install en npm run dev uit → Vite serveert de app op http://localhost:5173.
/insecure met payloads uit de documentatie; dezelfde stroom op /secure toont gesaniseerde uitvoer.payloadPresets.ts en simulateXssImpact.ts voor de concrete gevallen en "impact"-toewijzing.localStorage (zie 03-session-hijacking) zodat je kunt zien wat script in de pagina kan lezen; insecure-patterns suggereert ook localStorage.getItem('auth_token') in DevTools om het te inspecteren.Vergelijk /insecure vs /secure in Elements en Console: dezelfde componenten, verschillende verwerking van de tekenreeks voordat deze de DOM bereikt.
XSS is controle-flow: data die overgaat in interpreteren. Bescherming is typen bij de grens: platte tekst, veilige gestructureerde componenten of gesaniseerde HTML met een minimale allowlist—plus CSP en cookie-semantiek die beperken wat een afwijkend script nog kan doen. De playground maakt die grens zichtbaar: een onveilige weergavepad vs een gesaniseerde pad, plus vooraf ingestelde payloads en toegewezen "impact"-gedrag in de repository.