Direktiven, Nonces und Report-Only ohne Framework
Eine Content Security Policy reduziert das Risiko von Cross-Site-Scripting drastisch, wird aber in vielen PHP-Anwendungen entweder gar nicht oder als starre Kopie aus einem Tutorial eingesetzt. Wer die Policy stattdessen direkt aus PHP generiert, mit Nonces pro Request und einem kontrollierten Rollout über den Report-Only-Modus, bekommt einen Schutzmechanismus, der zur eigenen Anwendung passt statt sie zu blockieren.
Inhaltsverzeichnis
- 1. Warum eine Content Security Policy in PHP Sinn ergibt
- 2. Die wichtigsten CSP-Direktiven verstehen
- 3. Den Header direkt aus PHP setzen
- 4. Nonce-basierte Policy pro Request generieren
- 5. Hash-Allowlisting für statische Inline-Skripte
- 6. Report-Only-Modus und Violation Reporting
- 7. Schrittweiser Rollout in gewachsenen Anwendungen
- 8. Typische Fehler beim Einsatz von CSP in PHP
- 9. CSP-Strategien im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum eine Content Security Policy in PHP Sinn ergibt
Eine Content Security Policy ist ein HTTP-Header, der dem Browser mitteilt, aus welchen Quellen Skripte, Styles, Bilder und andere Ressourcen geladen werden dürfen. Anders als serverseitiges Output-Encoding wirkt eine Content Security Policy als zweite Verteidigungslinie: Selbst wenn eine XSS-Lücke im Code übersehen wurde, kann der Browser das Ausführen eines eingeschleusten Skripts trotzdem verhindern, weil es nicht der erlaubten Quelle entspricht. Genau diese Redundanz macht die Content Security Policy zu einem der wirksamsten einzelnen Security-Header überhaupt.
In PHP-Anwendungen wird die Content Security Policy häufig entweder komplett übersprungen, weil sie als reine Frontend-Angelegenheit missverstanden wird, oder als starre Kopie aus einem Blogpost eingesetzt, die eigene Inline-Skripte und Drittanbieter-Widgets sofort blockiert. Beides ist unbefriedigend. Der bessere Weg ist, die Content Security Policy als PHP-generierten Wert zu behandeln, der sich pro Request anpasst, Nonces für legitime Inline-Skripte enthält und über einen Report-Only-Modus kontrolliert eingeführt wird, bevor er scharf geschaltet wird.
2. Die wichtigsten CSP-Direktiven verstehen
Bevor man eine Content Security Policy aus PHP heraus zusammenbaut, muss klar sein, welche Direktiven zur Verfügung stehen. default-src ist der Fallback für alle nicht explizit gesetzten Ressourcentypen. script-src steuert, woher JavaScript geladen werden darf, und ist meist die kritischste Direktive, weil sie direkt gegen XSS wirkt. style-src regelt CSS-Quellen, img-src Bildquellen, connect-src die Ziele von fetch und XHR, und frame-ancestors ersetzt den älteren X-Frame-Options-Header für Clickjacking-Schutz.
Werte innerhalb einer Direktive werden durch Leerzeichen getrennt: 'self' erlaubt die eigene Origin, 'none' blockiert alles, konkrete Domains wie https://cdn.example.com erlauben genau diese Quelle. Das Schlüsselwort 'unsafe-inline' hebt den Inline-Schutz komplett auf und sollte in einer modernen Content Security Policy vermieden werden, weil es genau den Mechanismus deaktiviert, den die Policy eigentlich bieten soll. Stattdessen nutzt man Nonces oder Hashes, die im nächsten Abschnitt gezeigt werden.
<?php
declare(strict_types=1);
// Minimal but production-viable Content Security Policy directives
final class CspDirectives
{
/**
* Build the base directive map before nonces are injected.
*
* @return array<string, list<string>>
*/
public static function baseline(): array
{
return [
'default-src' => ["'self'"],
'script-src' => ["'self'"],
'style-src' => ["'self'"],
'img-src' => ["'self'", 'data:'],
'connect-src' => ["'self'"],
'frame-ancestors' => ["'none'"],
'object-src' => ["'none'"],
'base-uri' => ["'self'"],
];
}
}
3. Den Header direkt aus PHP setzen
Der eigentliche Header wird mit der PHP-Funktion header() gesetzt, bevor irgendein Output an den Browser gesendet wurde. Der Header-Name ist Content-Security-Policy, der Wert ist eine durch Semikolons getrennte Liste von Direktiven. Wichtig: header() muss vor dem ersten Byte an Ausgabe aufgerufen werden, sonst meldet PHP die Warnung "headers already sent" und die Content Security Policy wird nie gesendet. In modernen Anwendungen setzt man den Header deshalb zentral in einem Bootstrap oder einer Middleware, nicht verstreut in einzelnen Controllern.
Für Migrations- und Debugging-Zwecke gibt es die Alternative Content-Security-Policy-Report-Only, die im nächsten Abschnitt genauer behandelt wird. Beide Header können parallel gesendet werden: Ein strenger Report-Only-Header zum Testen einer neuen Content Security Policy und ein etwas lockerer scharfer Header, der bereits produktionstauglich ist. So lässt sich eine Verschärfung der Content Security Policy risikofrei vorbereiten, ohne die aktuelle Anwendung zu gefährden.
<?php
declare(strict_types=1);
/**
* Serialize directive array into a CSP header value
* and send it as an enforced Content-Security-Policy header.
*
* @param array<string, list<string>> $directives
*/
function sendCspHeader(array $directives): void
{
$parts = [];
foreach ($directives as $name => $sources) {
$parts[] = $name . ' ' . implode(' ', $sources);
}
// Must run before any output — no whitespace, no BOM before this call
header('Content-Security-Policy: ' . implode('; ', $parts));
}
$directives = CspDirectives::baseline();
sendCspHeader($directives);
4. Nonce-basierte Policy pro Request generieren
Ein Nonce ist ein zufälliger, einmal gültiger Wert, der pro Request neu erzeugt wird und sowohl im CSP-Header als auch als Attribut auf jedem erlaubten <script>-Tag erscheint. Nur Skripte mit passendem Nonce werden vom Browser ausgeführt, alle anderen Inline-Skripte, insbesondere von einem Angreifer eingeschleuste, werden blockiert. Für eine sichere Content Security Policy mit Nonces ist entscheidend, dass der Wert kryptografisch sicher erzeugt wird, üblicherweise mit random_bytes() und anschließender Base64-Kodierung.
Der Nonce darf niemals wiederverwendet werden, weder über mehrere Requests noch innerhalb desselben Requests für unterschiedliche Zwecke. Er wird einmal pro Seitenaufruf generiert, im Template an alle legitimen Inline-Skripte weitergereicht und gleichzeitig in die script-src-Direktive der Content Security Policy eingefügt. Wichtig ist außerdem, den Nonce nicht in einem vorhersehbaren Muster zu erzeugen, etwa aus einem Zeitstempel, weil das den gesamten Schutz aushebeln würde.
<?php
declare(strict_types=1);
final class CspNonceGenerator
{
private string $nonce;
public function __construct()
{
// 16 random bytes -> 128 bit of entropy, base64 encoded for the header
$this->nonce = base64_encode(random_bytes(16));
}
public function value(): string
{
return $this->nonce;
}
/**
* Build script-src directive value with the current request nonce.
*
* @return list<string>
*/
public function scriptSrc(): array
{
return ["'self'", sprintf("'nonce-%s'", $this->nonce)];
}
}
$csp = new CspNonceGenerator();
$directives = CspDirectives::baseline();
$directives['script-src'] = $csp->scriptSrc();
sendCspHeader($directives);
// In the template: <script nonce="<?= htmlspecialchars($csp->value()) ?>">...</script>
5. Hash-Allowlisting für statische Inline-Skripte
Nicht jedes Inline-Skript ändert sich pro Request. Für vollständig statische Inline-Skripte, etwa ein festes Tracking-Snippet, ist ein Hash oft praktischer als ein Nonce. Die Content Security Policy unterstützt 'sha256-...'-Werte, die den Base64-kodierten SHA-256-Hash des exakten Skriptinhalts enthalten. Ändert sich auch nur ein Zeichen im Skript, etwa durch versehentliche Formatierung, stimmt der Hash nicht mehr überein und der Browser blockiert das Skript.
Der Vorteil von Hashes gegenüber Nonces liegt darin, dass sie sich cachen lassen, weil sie nicht pro Request neu berechnet werden müssen. Der Nachteil: Jede Änderung am Skriptinhalt erfordert eine manuelle Aktualisierung des Hash-Werts in der Content Security Policy, was bei häufig wechselndem Code unpraktisch wird. In der Praxis kombiniert man deshalb Hashes für stabile Third-Party-Snippets mit Nonces für dynamisch generierte Inline-Skripte.
<?php
declare(strict_types=1);
/**
* Compute the CSP hash source expression for a static inline script.
*/
function cspHashSource(string $scriptContent): string
{
$hash = base64_encode(hash('sha256', $scriptContent, true));
return sprintf("'sha256-%s'", $hash);
}
$trackingSnippet = "console.log('page view tracked');";
$directives = CspDirectives::baseline();
$directives['script-src'] = ["'self'", cspHashSource($trackingSnippet)];
sendCspHeader($directives);
// Any modification of $trackingSnippet — even whitespace — invalidates the hash
6. Report-Only-Modus und Violation Reporting
Bevor eine strenge Content Security Policy scharf geschaltet wird, empfiehlt sich der Header Content-Security-Policy-Report-Only. Er blockiert nichts, meldet aber jede Verletzung an eine konfigurierte report-uri oder das modernere report-to-Endpunkt-Konzept. Damit lässt sich in Produktion beobachten, welche Ressourcen tatsächlich geladen werden, ohne dass ein zu strenges Regelwerk die Anwendung für echte Nutzer bricht.
Der Report-Only-Endpunkt in PHP nimmt einen JSON-Payload per POST entgegen und sollte die Verletzungen strukturiert loggen, etwa nach blockierter Direktive und blockierter URI gruppiert. Nach einigen Tagen im Report-Only-Modus zeigt sich meist ein klares Bild: Welche Drittanbieter-Skripte fehlen noch in der Content Security Policy, welche Inline-Handler im Legacy-Code aufgeräumt werden müssen. Erst danach wird aus dem Report-Only-Header ein scharf geschalteter Header.
<?php
declare(strict_types=1);
// csp-report-endpoint.php — receives browser CSP violation reports
header('Content-Type: application/json');
$raw = file_get_contents('php://input');
/** @var array{'csp-report': array<string, mixed>}|null $payload */
$payload = json_decode($raw ?: '', true);
if (isset($payload['csp-report'])) {
$report = $payload['csp-report'];
error_log(sprintf(
'[CSP-VIOLATION] directive=%s blocked=%s document=%s',
$report['violated-directive'] ?? 'unknown',
$report['blocked-uri'] ?? 'unknown',
$report['document-uri'] ?? 'unknown'
));
}
http_response_code(204);
7. Schrittweiser Rollout in gewachsenen Anwendungen
In einer gewachsenen PHP-Anwendung mit vielen Jahren an Legacy-Code lässt sich eine strenge Content Security Policy selten von einem Tag auf den anderen einführen. Der bewährte Rollout-Pfad beginnt mit einer sehr permissiven Policy im Report-Only-Modus, die zunächst nur beobachtet. Anschließend wird Bereich für Bereich verschärft: zuerst object-src 'none' und frame-ancestors, weil diese selten legitime Ausnahmen brauchen, danach script-src mit Nonces für die kritischsten Seiten wie Login und Checkout.
Ein pragmatischer Zwischenschritt ist, die Content Security Policy zunächst nur für neue Features scharf zu schalten und den Rest der Anwendung im Report-Only-Modus zu belassen, bis die gemeldeten Verletzungen abgearbeitet sind. Wichtig ist dabei, das Reporting nicht zu ignorieren, sondern die gesammelten Daten regelmäßig auszuwerten und in die nächste Verschärfungsstufe der Content Security Policy einfließen zu lassen.
8. Typische Fehler beim Einsatz von CSP in PHP
Der häufigste Fehler ist 'unsafe-inline' aus Bequemlichkeit stehen zu lassen, weil sonst bestehende Inline-Skripte kaputtgehen. Damit wird die Content Security Policy gegen genau die Angriffsklasse wirkungslos, die sie eigentlich verhindern soll. Ein zweiter Fehler ist, den Header nach bereits gesendetem Output zu setzen, was in PHP eine "headers already sent"-Warnung erzeugt und den Header stillschweigend verwirft, wenn Fehler nicht in Logs sichtbar gemacht werden.
Ein dritter Fehler betrifft die Nonce-Wiederverwendung über mehrere Requests hinweg, etwa durch Caching des gerenderten HTML inklusive Nonce-Wert. Wird eine gecachte Seite mit demselben Nonce mehrfach ausgeliefert, kann ein Angreifer den Wert aus einer früheren Antwort wiederverwenden und die Content Security Policy umgehen. Nonces und Full-Page-Caching vertragen sich nur, wenn der Cache das Skript-Tag mit dem Nonce dynamisch pro Auslieferung neu einsetzt, nicht als statischen Bestandteil der gecachten Seite.
9. CSP-Strategien im direkten Vergleich
Je nach Anwendungsfall eignen sich unterschiedliche Mechanismen innerhalb einer Content Security Policy unterschiedlich gut. Die folgende Übersicht zeigt, wann welcher Ansatz die bessere Wahl ist.
| Mechanismus | Unsicher / Ineffektiv | Empfohlener Ansatz | Vorteil |
|---|---|---|---|
| Dynamisches Inline-Skript | 'unsafe-inline' |
'nonce-...' pro Request |
Angreifer kann Nonce nicht erraten |
| Statisches Tracking-Snippet | 'unsafe-inline' |
'sha256-...'-Hash |
Cachefähig, kein Request-Overhead |
| Rollout in Legacy-App | Sofort scharf schalten | Report-Only zuerst | Keine überraschenden Ausfälle |
| Clickjacking-Schutz | Nur X-Frame-Options | frame-ancestors 'none' |
Feingranularer, ersetzt Legacy-Header |
| Verletzungen erkennen | Manuelles Testen | Report-Endpunkt mit Logging | Sichtbarkeit über echte Nutzer |
In der Praxis überschneiden sich diese Mechanismen: Eine ausgereifte Content Security Policy kombiniert Nonces für dynamische Inline-Skripte, Hashes für stabile Snippets und einen dauerhaft aktiven Report-Endpunkt, der auch nach dem Rollout weiter Verletzungen protokolliert, damit neue Regressionen sofort auffallen.
Mironsoft
PHP-Security-Audits, Header-Härtung und Magento/Hyvä-Integration
Content Security Policy ohne Ausfälle einführen?
Wir analysieren bestehende PHP-Anwendungen, bauen eine passgenaue Content Security Policy mit Nonces und Hashes und begleiten den Rollout über Report-Only bis zur scharf geschalteten Policy, ohne dass produktive Features brechen.
CSP-Audit
Bestandsaufnahme aller Inline-Skripte, Third-Party-Quellen und Ausnahmen
Nonce-Integration
Request-basierte Nonces in Templates und Middleware sauber verdrahten
Report-Only-Rollout
Kontrollierte Verschärfung mit Violation-Monitoring statt Big-Bang-Umstellung
10. Zusammenfassung
Eine Content Security Policy aus PHP heraus zu setzen ist keine reine Header-Kopie aus dem Internet, sondern ein Prozess: Direktiven definieren, den Header vor jedem Output senden, Nonces pro Request für dynamische Inline-Skripte generieren und Hashes für stabile Snippets nutzen. Der Report-Only-Modus erlaubt einen risikofreien Test in Produktion, bevor die Content Security Policy tatsächlich blockiert. In gewachsenen Anwendungen erfolgt der Rollout schrittweise, Direktive für Direktive, mit kontinuierlichem Violation-Reporting statt einer einmaligen Big-Bang-Umstellung.
Der größte Gewinn einer sauber implementierten Content Security Policy liegt in der zweiten Verteidigungslinie gegen XSS: Selbst wenn eine Schwachstelle im Code übersehen wurde, verhindert der Browser die Ausführung des eingeschleusten Skripts, weil es weder Nonce noch Hash noch erlaubte Quelle entspricht. Diese Redundanz kostet in PHP wenige Zeilen Code, verhindert aber ganze Angriffsklassen zuverlässig.
Content Security Policy aus PHP heraus setzen — Das Wichtigste auf einen Blick
Header setzen
header('Content-Security-Policy: ...') vor jedem Output senden, zentral in Bootstrap oder Middleware statt verstreut in Controllern.
Nonce statt unsafe-inline
Pro Request mit random_bytes() erzeugen, nie wiederverwenden, nie mit statischem Full-Page-Cache kombinieren.
Hash für statische Snippets
sha256-Hash des exakten Skriptinhalts, cachefähig, aber manuell zu pflegen bei Codeänderungen.
Report-Only zuerst
Verletzungen beobachten, bevor scharf geschaltet wird. Reporting auch danach dauerhaft aktiv lassen.