Content Security Policy aus PHP heraus setzen
AI generated
<?php
8.4
PHP · Security · HTTP Header · XSS-Schutz
Content Security Policy aus PHP heraus setzen
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.

17 Min. Lesezeit header() · Nonce · Hash · Report-Only PHP 8.x · Framework-unabhängig

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.

11. FAQ: Content Security Policy aus PHP heraus setzen

1Warum CSP, wenn der Code XSS-sicher ist?
Zweite Verteidigungslinie. Übersehene Lücken oder Drittanbieter-Fehler werden trotzdem vom Browser blockiert, weil das Skript keine erlaubte Quelle hat.
2Meta-Tag statt Header möglich?
Nur eingeschränkt. frame-ancestors und report-uri funktionieren nur als echter HTTP-Header über header().
3Nonce funktioniert nicht mit Page-Cache?
Gecachte Seiten liefern denselben Nonce mehrfach aus. Nonce muss bei jeder Auslieferung dynamisch neu eingesetzt werden.
4Ist unsafe-inline jemals okay?
Nur als kurzer Übergang während der Migration, nie dauerhaft. Es hebt den Inline-Schutz komplett auf.
5Wie teste ich eine neue Policy sicher?
Content-Security-Policy-Report-Only nutzen. Blockiert nichts, meldet aber alle Verletzungen an einen Report-Endpunkt.
6Wie generiere ich einen sicheren Nonce?
base64_encode(random_bytes(16)). Nie aus Zeitstempel oder vorhersehbarer Quelle ableiten.
7Nonce oder Hash, was wann?
Nonce für dynamische Inline-Skripte pro Request. Hash für unveränderliche, statische Snippets, cachefähig.
8Brauche ich noch X-Frame-Options?
Als Fallback ja. frame-ancestors ist moderner und feingranularer, aber ältere Clients ignorieren es teilweise.
9Warum wird mein Header nicht gesendet?
header() wurde nach bereits gesendetem Output aufgerufen. Muss vor jeder Ausgabe stehen, auch vor versehentlichen Leerzeichen.
10Wie behandle ich Drittanbieter-Skripte?
Explizit in der passenden Direktive erlauben, z.B. script-src https://cdn.example.com. Skripte, die selbst Inline-Code nachladen, vorher prüfen.