
Passwort vergessen
Wie XSS den DOM erreicht, welche Browser-APIs Senken sind und welche Abwehrmaßnahmen in der Produktion greifen—Säuberung, CSP, Cookies und CSRF-Pairing.
Wenn ein String als Markup oder Skript anstatt als inert Text interpretiert wird, läuft dieser Inhalt als Code im Ursprung deiner Seite—mit denselben Rechten wie dein Bundle. Das ist XSS: kein Styling-Fehler, ein verwirrter Stellvertreter zwischen Daten und Anweisungen. Dieser Beitrag ist die technische Grundlage—Angriffsmechanik, Senken, Skalierbare Fixes, plus eine ausführbare frontend-xss-demo (Vite + React auf main; live: xss.lucascoliveira.com) und Hinweise auf dieses Next.js-Repo, wo Header und MDX das Standardrisiko reduzieren.
In Skripte, die in deine Seite injiziert werden, laufen mit dem gleichen Ursprung wie dein Bundle. In der Praxis bedeutet das oft:
document.cookie-Sichtbarkeit für Cookies, die nicht als HttpOnly markiert sind (Sitzungdiebstahl / Fixierungsketten).fetch() / XMLHttpRequest zu deiner API mit Umgebungsberechtigungen, wenn Cookies gesendet werden und CORS dies zulässt—oder Exfiltration von Token, die in localStorage / sessionStorage gehalten werden, wenn deine App sie dort platziert hat.Die Abwehr ist also nicht „verwende React“—es ist nie die Zuweisung von angreiferkontrollierten Bytes an eine Senke, die HTML parst oder Skript ausführt, es sei denn, ein geprüfter Pipeline hat sie auf einen sicheren Typ reduziert.
Gespeicherte XSS — Unvertrauenswürdiges HTML (oder eine Payload, die zu HTML wird, nachdem die Vorlage erweitert wurde) wird persistiert (DB, Cache, Suchindex). Jeder Benutzer, der diesen Datensatz lädt, trifft auf die Senke. Frontend-Auswirkung: das erste Rendern, das innerHTML = row.body oder ähnlich ohne Säuberung ausführt, führt die Payload aus.
Reflektierte XSS — Die Payload persistiert nie; sie springt vom Server oder der Routing-Schicht in die Antwort. Klassisch: ?q=<script>…</script> reflektiert in das HTML ohne Kodierung. SPA-Äquivalent: location.search oder Hash, der clientseitig geparsed und ohne Kodierung in den DOM geschrieben wird. Die Fixierung ist dieselbe: behandle URL-abgeleitete Strings als Daten; wenn du sie reflektieren musst, kodiere für den Kontext (siehe unten).
DOM-basierte XSS — Die Serverantwort ist „sauber“, aber clientseitiges Skript liest angreiferkontrollierte Eingabe (location, referrer, postMessage, WebSocket-Nachrichten) und übergibt sie an eine Senke. Beispiel: eval("handle" + location.hash.slice(1)) oder element.innerHTML = decodeURIComponent(...). Statische Analyse von Vorlagen reicht nicht aus; du musst jeden Pfad von unvertrauenswürdiger Eingabe zu Senke prüfen.
Dies sind die üblichen Schuldigen in React/SPA-Codebasen:
| Senke | Risiko |
|---|---|
element.innerHTML, insertAdjacentHTML | Parst HTML; jeder Tag/Ereignishandler, den du zulässt, kann Skript ausführen. |
dangerouslySetInnerHTML | Dasselbe wie oben—React säubert nicht. |
document.write | Dasselbe. |
eval, new Function, setTimeout(string) | Direkte Skriptausführung. |
javascript:-URLs in href / src | Navigation oder Ressourcenladung, die als Skript-URL ausgeführt wird. |
postMessage-Handler, die eval oder HTML aus event.data setzen | XSS, wenn origin nicht validiert wird oder event.data eine Senke ohne sicheren Vertrag erreicht—nicht nur „falsche Fenster“-Fehler. |
Keine Senke per Default: textContent, createTextNode, Reacts normale Kind-Textdaten, Attribute, die React als Strings behandelt, wenn du sie nicht umgehst. Markdown-Pipelines werden zu Senken, wenn sie rohes HTML emittieren und du dieses HTML unsaniert dem DOM zuweist.
Demo-Hinweis (wichtig): In HTML5 werden <script>-Knoten, die über innerHTML / dangerouslySetInnerHTML eingefügt werden, nicht ausgeführt—der Parser führt sie nicht aus, wie ein klassischer reflektierter XSS es tun könnte. Um Ausführung zu sehen, wenn HTML injiziert wird, verlassen sich Payloads typischerweise auf Attribut-Handler (z. B. onerror auf img) oder ähnliches. Die insecure-patterns-Dokumentation im Demo-Repo erläutert dies, damit du die App mit Beispielen testest, die tatsächlich nach der Insertion ausführen.
Wenn die Benutzeroberfläche nur einfachen Text benötigt — Binde Text mit textContent, React-Textkindern oder MDX, das zu Komponenten kompiliert wird ohne einen HTML-Pipeline. Kein Säuberungsprogramm erforderlich; du bist nicht im HTML-Spiel.
Wenn du reichhaltigen Text benötigst (fett, Listen, Links) — Du benötigst entweder eine eingeschränkte Markup-Sprache, die zu sicheren Elementen kompiliert wird oder HTML-Säuberung mit einer Zulassungsliste (Tags + Attribute). Kodierung (z. B. HTML-Entitätskodierung) ist für das Einfügen von Daten in HTML-Textknoten; Säuberung ist für wenn du eine Teilmenge von HTML zulassen musst. Verwechsele die beiden nicht.
Tiefenverteidigung für reichhaltigen Text in realen Produkten — Validieren/Säubern beim Schreiben (API lehnt unbekannte Tags, Längenlimits ab) und säubern oder rendern durch einen sicheren Pfad beim Lesen (Render-Schicht). Speicher kann zurückgesetzt, korrupt oder von einer anderen Serviceversion geschrieben werden.
DOMPurify ist ein Browser-Säuberungsprogramm mit einem Standardprofil; du konfigurierst es dennoch für dein Produkt:
ALLOWED_TAGS / ALLOWED_ATTR — Beginne minimal (p, br, strong, em, a mit href nur, wenn du Links benötigst). Jedes zusätzliche Tag ist Angriffsfläche.ADD_ATTR / FORBID_TAGS — Explizit schlägt „fast alles zulassen“ vor.RETURN_DOM / RETURN_TRUSTED_TYPE — Bevorzuge DOM-Knoten oder TrustedHTML-ähnliche Ausgabe, wenn du mit Trusted Types integrierst.afterSanitizeAttributes — Streiche href-Werte, die mit javascript: beginnen oder seltsame data:-MIME-Typen, wenn du Links zulässt.Im frontend-xss-demo auf main läuft die sichere Route (/secure) Todo-Text durch DOMPurify, bevor dangerouslySetInnerHTML; die unsichere Route (/insecure) tut dies nicht—selbe Benutzeroberfläche, andere Vertrauensrichtlinie. Portugiesisch /seguro und /inseguro existieren noch als Legacy-Aliase und leiten zu /secure und /insecure um.
CSP reduziert was ausgeführt werden kann, wenn etwas durchkommt. In apps/web/next.config.ts setzt diese Seite eine CSP mit default-src 'self', engem object-src 'none', base-uri 'self', form-action 'self', frame-ancestors 'none', plus script-src / style-src mit 'unsafe-inline', weil Next.js App Router + MUI sx derzeit Inline-Skript/ Stil in diesem Setup benötigen—dokumentiert im Code. Nonce- oder Hash-basiertes script-src würde die breite Inline-Skriptzulassung entfernen, aber Middleware erfordern, um Noncen pro Anfrage zu injizieren—wert, wenn du selten Benutzer-HTML versendest.
Realitätscheck: CSP ersetzt nicht die Säuberung für Benutzer-HTML; es verengt den Schadensradius (z. B. kann Skript-Hosts blockieren, die du nicht zugelassen hast). Inline-Ereignishandler (onerror usw.) werden nicht automatisch neutralisiert, nur weil du eine CSP setzt—'unsafe-inline' auf script-src ist in realen Apps üblich (einschließlich dieses Next-/MUI-Setups), und Blockierung von Handlern erfordert normalerweise explizite script-src / script-src-attr (oder Noncen/Hashes), je nach Browser und CSP-Ebene.
XSS kann CSRF-Token umgehen, wenn der Token vom DOM lesbar ist oder wenn das Angreifer-Skript Anfragen mit Berechtigungen ausführt. Also: priorisiere XSS-Fixes; auch:
HttpOnly, Secure, SameSite=Lax oder Strict dort, wo Flüsse es erlauben—reduziert Cross-Site-Kookie-Leckage und klassischen CSRF.SameSite-Cookies, Anti-CSRF-Tokenen oder benutzerdefinierten Headern + CORS-Richtlinie, damit zufällige Seiten keine authentifizierten Anfragen ausführen können.Frontend-Aufgabe: füge keine Geheimnisse in JS-lesbaren Speicher, wenn vermeidbar; verwende fetch mit expliziter credentials-Richtlinie, die mit deinem API-Design übereinstimmt.
apps/web/next.config.ts: Sicherheitsheader auf /(.*); CSP-String, der im Code mit env-spezifischem script-src (Entwicklung unsafe-eval für React-Stacks nur dort, wo benötigt) erstellt wird.apps/web/app/api/chat/route.ts: JSON-Parsing, leere Überprüfung, MAX_MESSAGE_LENGTH-Kap—Missbrauchsformung, kein XSS an sich.apps/web/lib/blog/mdx.tsx: MDX mit einer festen Komponentenkarte (next-mdx-remote/rsc), keine rohen HTML-Strings aus CMS. Verschiedene Bedrohungsmodelle als „ Nachrichtenrumpf mit HTML“.main dir gibtKlon frontend-xss-demo, führe npm install und npm run dev aus → Vite dient der App bei http://localhost:5173.
/insecure mit Payloads aus den Dokumenten hinzu; derselbe Fluss auf /secure zeigt gesäuberte Ausgabe.docs/xss/: eine Übersicht, drei Auswirkungsdurchläufe (Aktionen ohne Benutzereingriff, interne Phishing-Angriffe, Sitzung Hijacking / Speicher), plus unsichere Muster und sichere Muster für codeebene Dos und Donts.localStorage (siehe 03-session-hijacking) damit du sehen kannst, was Skript in der Seite lesen kann; unsichere Muster schlagen auch localStorage.getItem('auth_token') in DevTools vor, um es zu überprüfen.Vergleiche /insecure vs. /secure in Elementen und Konsole: dieselben Komponenten, unterschiedliche Behandlung des Strings, bevor er den DOM erreicht.
rg "dangerouslySetInnerHTML|innerHTML|insertAdjacentHTML|eval\\(|new Function" in deiner App und Abhängigkeiten.<img src=x onerror=...>, javascript:-URLs—und erinnere dich innerHTML führt <script> nicht aus, wie viele Cheat Sheets implizieren. Für innerHTML. Injektionsdemos bevorzuge onerror auf img (oder ähnlich); <svg onload> ist oft unzuverlässig, wenn auf diese Weise eingefügt.SameSite, Berechtigungen und CSRF-Strategie mit Backend aus; gehe davon aus, dass XSS und CSRF gekettet werden.XSS ist Kontrollfluss: Daten, die in Interpretation übergehen. Schutz ist Typisierung an der Grenze: einfacher Text, sichere strukturierte Komponenten oder gesäubertes HTML mit einer minimalen Zulassungsliste—plus CSP und Cookie-Semantik, die begrenzen, was ein streunendes Skript noch tun kann. Die Demo auf main macht diese Grenze sichtbar: /insecure vs. /secure, dokumentierte Auswirkungen unter docs/xss/ und Disziplin in jedem PR, der Zeichen nahe dem DOM berührt.