flawopen.com/xss/Javascript
JavaScript-XSS: DOM, React und Express absichern
Nutzen Sie Text-APIs, bereinigen Sie HTML, prüfen Sie URLs und erzwingen Sie Trusted Types für DOM/React (CWE-79).
Stellen Sie sich ein Regiepult im Theater vor. Behandelt die Inspizienz jeden Zettel aus dem Publikum als Bühnenanweisung, kann ein Zettel eine unerwartete Szene hinzufügen; als gewöhnlicher Dialog angezeigt bleibt er Text. Im Browser weisen innerHTML und Reacts dangerouslySetInnerHTML den DOM an, den gelieferten Text als Markup zu parsen, während textContent ihn wörtlich darstellt. Maskieren Sie normalen Text, bereinigen Sie nur ausdrücklich erlaubtes HTML und prüfen Sie URL-Schemata vor dem Erzeugen von Links.
Kernkonzepte & Begriffe
innerHTML vs textContent- Eine Zuweisung an
innerHTML,outerHTMLoderinsertAdjacentHTMLlässt den Browser den String als HTML parsen, ein darin enthaltenes<img onerror>wird also zu einem aktiven Element.textContentundcreateTextNodefügen nur Text ein; nichts im Wert wird je als Markup geparst. Template literals in res.send()- Express'
res.send()schickt Strings standardmäßig alstext/html. Ein Template-Literal wertet${req.query.q}vor dem Senden aus, die Antwort enthält also, was der Nutzer in den Query-String geschrieben hat.req.query.qkann auch ein Array oder Objekt sein, also den Typ prüfen. dangerouslySetInnerHTML + DOMPurify- React maskiert Text in JSX automatisch;
dangerouslySetInnerHTMList der ausdrückliche Ausstieg. Den Wert durchDOMPurify.sanitize()schicken, das ihn mit dem browsereigenen Parser liest und Skripte, Event-Handler und gefährliche URLs entfernt, bevor React ihn einfügt. javascript: URLs in href- JSX maskiert den Attributwert, aber
href="javascript:..."ist eine gültige URL, die beim Klick Code ausführt. Mitnew URL()parsen und nurhttp:undhttps:erlauben; der URL-Parser entfernt führende Leerzeichen und Tabs, die einfache Präfixprüfungen übersehen. Trusted Types- Mit der CSP-Direktive
require-trusted-types-for 'script'lehnen unterstützende Browser einfache Strings in Sinks wieinnerHTMLab und akzeptieren nurTrustedHTML-Objekte aus einer selbst definierten Policy, typischerweise einer, die DOMPurify aufruft. Neue Sinks rutschen nicht mehr unbemerkt durch.
Framework-Zuordnungen
- CWE-Referenz für die in diesem Leitfaden beschriebene Schwachstelle: CWE-79
- OWASP Top 10:2025 — A05:2025 Injection
Schritt-für-Schritt Angriffsablauf
Die Eingabe erreicht einen Sink
Eine Suchanfrage, ein per API geladenes Profilfeld oder ein von einem anderen Nutzer gespeicherter Link kommt als String an und geht an innerHTML, dangerouslySetInnerHTML, href oder res.send().
Der Wert wird zu Markup
Mit dem harmlosen Testwert <img src=x onerror=alert(1)> erzeugen innerHTML und res.send() ein echtes <img>-Element; ein gespeicherter Link javascript:alert(1) wird zu einem anklickbaren Skript.
Der Browser führt es aus
Das Bild lädt nicht und löst onerror aus, oder der Nutzer klickt den Link. Der Code läuft sofort, ohne weiteres Zutun des Angreifers.
Das Skript handelt als die Website
Es läuft mit dem Origin der Seite: Es kann das DOM lesen, die APIs der Website mit den Cookies des Opfers oder Tokens aus localStorage aufrufen und ändern, was der Nutzer sieht.
Quellcode: Verwundbar vs. Sicher
// profile.jsx: jede Funktion unten lässt einen String als HTML parsen
import express from 'express';
const app = express();
// Express: der Query-Wert landet vor dem Senden im HTML
app.get('/search', (req, res) => {
res.send(`<h1>Results for ${req.query.q}</h1>`);
});
// innerHTML parst name als Markup
export function showGreeting(el, name) {
el.innerHTML = `Hello, ${name}`;
}
// React fügt das HTML genau so ein, wie es ankam
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: html }} />;
}
// eine javascript:-URL läuft beim Klick auf den Link
export function Website({ url }) {
return <a href={url}>Website</a>;
}
// profile.jsx: Text bleibt Text; HTML und URLs werden zuerst geprüft
import DOMPurify from 'dompurify';
import escapeHtml from 'escape-html';
import express from 'express';
const app = express();
// nur Strings werden akzeptiert, und zwar HTML-maskiert
app.get('/search', (req, res) => {
const q = typeof req.query.q === 'string' ? req.query.q : '';
res.send(`<h1>Results for ${escapeHtml(q)}</h1>`);
});
// textContent parst nie Markup
export function showGreeting(el, name) {
el.textContent = `Hello, ${name}`;
}
// DOMPurify entfernt Skripte und Event-Handler vor dem Einfügen
export function Bio({ html }) {
return <div className="bio" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}
// nur http- und https-URLs bleiben übrig, alles andere wird zu #
function safeHref(url) {
try {
const parsed = new URL(url, window.location.origin);
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
} catch {
return '#';
}
}
export function Website({ url }) {
return <a href={safeHref(url)}>Website</a>;
}
Checkliste für Engineering & Systemsicherheit
- ✓
react/no-dangerundeslint-plugin-no-unsanitizedim CI aktivieren undinnerHTML,outerHTML,insertAdjacentHTMLsowiedocument.writemit Nicht-Literalen blockieren. - ✓Für Text
textContentverwenden; jedes HTML, das gerendert werden muss, beim Einfügen durchDOMPurify.sanitize()schicken. - ✓Jede nutzergelieferte URL mit
new URL()und einerhttp:/https:-Allowlist prüfen, bevor siehref,srcoderwindow.locationerreicht. - ✓In Express über eine automatisch maskierende View-Engine oder
escape-htmlrendern und Query-Parameter ablehnen, die keine Strings sind. - ✓Eine CSP mit Nonce-basiertem
script-src, ohne'unsafe-inline'und mitrequire-trusted-types-for 'script'senden; Session-Tokens inHttpOnly-Cookies statt inlocalStoragehalten.