Webdesign 15 Min. Lesezeit

Content Security Policy in modernen Web-Apps: Hashes, nonces & strict-dynamic

CSP richtig einsetzen: Hash-basierte vs. nonce-basierte Direktiven, strict-dynamic für Third-Party-Scripts, report-only Modus und der CSP-Workflow in Astro-Projekten.

Content Security Policy (CSP) ist eine der effektivsten Verteidigungslinien gegen Cross-Site-Scripting (XSS). Aber eine falsch konfigurierte CSP ist entweder wirkungslos oder bricht die eigene Anwendung. Dieser Artikel erklärt die Konzepte — Hashes, Nonces, strict-dynamic — und zeigt einen produktionstauglichen Workflow für Astro-Projekte.

Warum 'unsafe-inline' keine Option ist

Eine CSP mit script-src 'unsafe-inline' ist de facto keine CSP. Sie erlaubt die Ausführung beliebiger Inline-Scripts — exakt das, was ein XSS-Angriff ausnutzen will.

# Wirkungslos — erlaubt allen Inline-Script-Ausführung:
Content-Security-Policy: script-src 'self' 'unsafe-inline'

# Sinnvoll — nur explizit erlaubte Scripts:
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-abc123'

Das Problem: viele Frameworks, Analytics-Tools und CMS injizieren Inline-Scripts. Die Lösung sind Hashes oder Nonces.

Hash-basierte CSP

Ein Hash erlaubt genau das Script, dessen Inhalt dem Hash entspricht. Ändert sich das Script, ist der Hash ungültig — der Browser blockiert die Ausführung.

# SHA-256 Hash des genauen Script-Inhalts:
Content-Security-Policy: script-src 'sha256-base64encodedHash='

In Astro können Hashes automatisch generiert werden. Ein Workflow mit einem Build-Script:

// scripts/generate-csp-hashes.mjs
import { createHash } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';
import { glob } from 'glob';
import { parse } from 'node-html-parser';

const distFiles = await glob('dist/**/*.html');
const hashes = new Set();

for (const file of distFiles) {
  const html = readFileSync(file, 'utf-8');
  const root = parse(html);

  root.querySelectorAll('script:not([src])').forEach((script) => {
    const content = script.text.trim();
    if (!content) return;
    const hash = createHash('sha256').update(content).digest('base64');
    hashes.add(`'sha256-${hash}'`);
  });
}

const cspValue = `script-src 'self' ${[...hashes].join(' ')};`;
console.log(cspValue);
// Output in _headers schreiben oder in Netlify-Config eintragen

Der Hash-Ansatz ist besonders geeignet für statisch generierte Sites (SSG), bei denen sich der Script-Inhalt zwischen Deploys nicht ändert.

Nonce-basierte CSP

Ein Nonce (Number used Once) ist ein kryptografisch zufälliger Wert, der pro Request generiert wird. Nur Scripts mit dem gleichen Nonce-Wert dürfen ausgeführt werden.

# Server setzt Header mit zufälligem Nonce:
Content-Security-Policy: script-src 'nonce-r4nd0m8yt3s'
<!-- Script muss denselben Nonce haben -->
<script nonce="r4nd0m8yt3s">
  // Erlaubt
</script>

<script>
  // Blockiert — kein Nonce
</script>

Nonces erfordern Server-Side-Rendering — bei statischen Sites ist Hash der richtige Ansatz.

In Astro mit SSR-Adapter (Netlify):

---
// layouts/Layout.astro
const nonce = crypto.randomUUID().replace(/-/g, '');
const csp = `default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';`;
---

<html>
  <head>
    <!-- CSP als meta-Tag oder besser als HTTP-Header -->
    <meta http-equiv="Content-Security-Policy" content={csp} />
  </head>
  <body>
    <script nonce={nonce}>
      // Erlaubt, weil Nonce übereinstimmt
      window.__INIT__ = true;
    </script>
    <slot />
  </body>
</html>

Wichtig: CSP als HTTP-Header ist stärker als als <meta>-Tag — HTTP-Header werden vor dem HTML-Parsing verarbeitet. Für Netlify-Projekte gehört die CSP in public/_headers.

strict-dynamic: das Schlüsselfeature für Third-Party-Scripts

'strict-dynamic' löst das größte praktische Problem mit CSP: Third-Party-Scripts, die selbst weitere Scripts laden.

Ohne strict-dynamic muss jede Third-Party-Domain explizit in der CSP stehen — und jedes dynamisch geladene Script ebenfalls. Das ist nicht praktikabel und wird oft zur Whitelist-Sammlung, die jeden XSS-Angriff erlaubt, der über eine gewhitelistete Domain kommt.

Mit 'strict-dynamic' gilt: wenn ein Script mit gültigem Nonce oder Hash weitere Scripts lädt, vertraut der Browser auch diesen geladenen Scripts — aber nicht anderen Inline-Scripts.

Content-Security-Policy:
  script-src 'nonce-abc' 'strict-dynamic';
<!-- Basis-Script mit Nonce -->
<script nonce="abc" src="https://analytics.example.com/loader.js"></script>

<!-- loader.js lädt dynamisch weitere Scripts -->
<script>
  const s = document.createElement('script');
  s.src = 'https://analytics.example.com/tracker.js';
  document.head.appendChild(s);
  // Erlaubt mit strict-dynamic — kein explizites Allow nötig
</script>

Report-Only: CSP ohne Produktionsrisiko

Bevor man eine strikte CSP produktiv schaltet, empfiehlt sich der Content-Security-Policy-Report-Only-Header:

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-abc'; report-uri /csp-violations

Der Browser führt alle Requests normal durch, meldet aber Verstöße gegen die Policy an den report-uri-Endpunkt. So kann man sehen, was blockiert würde, ohne die Seite zu brechen.

Für Astro/Netlify-Projekte ohne eigenen Server kann man CSP-Violations an einen externen Service wie report-uri.com senden.

CSP in Astro-Projekten: praktischer Workflow

1. Build-Phase: Hashes generieren

# Nach dem Build:
npm run build

# Hashes aus dist/ extrahieren:
node scripts/generate-csp-hashes.mjs >> csp-hashes.txt

2. _headers-Datei für Netlify

# public/_headers
/*
  Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-HASH1=' 'sha256-HASH2='; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.brevo.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
  X-Content-Type-Options: nosniff
  X-Frame-Options: DENY
  Referrer-Policy: strict-origin-when-cross-origin
  Permissions-Policy: camera=(), microphone=(), geolocation=()

3. Problematische Direktiven identifizieren

Häufige Quellen von CSP-Verstößen in Astro-Projekten:

  • Inline-Event-Handler (onclick="...") — durch JavaScript-Events ersetzen
  • style-Attribute — durch Tailwind-Klassen ersetzen (Pflicht für 'unsafe-inline'-Freiheit)
  • Dynamisch generierte Scripts (z.B. von React-Hydratisierung) — Nonce oder Hash nötig
  • Google Fonts — Self-Hosting oder font-src fonts.gstatic.com whitelist

Was man nicht tun sollte

Anti-Feature: X-XSS-Protection-Header setzen

# FALSCH — dieser Header ist veraltet und riskant:
X-XSS-Protection: 1; mode=block

# Er ist in modernen Browsern nicht mehr vorhanden.
# In alten Edge/IE-Versionen kann er mXSS-Angriffe ermöglichen.
# CSP ersetzt ihn vollständig. Einfach weglassen.

Anti-Feature: Wildcard in script-src

# FALSCH:
script-src 'self' *.googleapis.com *.gstatic.com

# Diese Whitelist erlaubt beliebige Scripts von Google-Domains —
# ein Angreifer, der eine beliebige Google-Subdomain kompromittiert,
# kann XSS einschleusen.

Weiterführende Artikel


Security-Audit für Ihre Web-App? Wender Media prüft CSP-Konfigurationen und HTTP-Sicherheitsheader — systematisch und ohne Ausfallrisiko. Anfrage an info@wendermedia.info.

Individuelle Beratung gewünscht?

Wender Media unterstützt Sie bei der praktischen Umsetzung — von der technischen Konzeption bis zum Launch. Schreiben Sie uns, wir antworten innerhalb von 24 Stunden.

Jetzt Beratung anfragen

Kostenlos & unverbindlich — info@wendermedia.info

Verwandte Artikel