CSRF-Schutz ohne Framework implementieren
AI generated
<?php
8.4
PHP · Security · Sessions · Formulare
CSRF-Schutz ohne Framework implementieren
Synchronizer Token, Double-Submit-Cookie und SameSite kombiniert

Wer eigene PHP-Anwendungen ohne Symfony oder Laravel betreibt, muss CSRF-Schutz selbst bauen. Mit random_bytes für die Token-Generierung, hash_equals für den zeitkonstanten Vergleich und einer sauberen Session-Bindung lässt sich ein robuster CSRF-Schutz in wenigen Zeilen PHP umsetzen, ganz ohne fremde Abhängigkeit.

18 Min. Lesezeit random_bytes · hash_equals · Session · SameSite PHP 8.1+

1. Was CSRF ist und warum eigener Schutz nötig ist

CSRF (Cross-Site Request Forgery) ist ein Angriff, bei dem eine bösartige Webseite den Browser eines eingeloggten Nutzers dazu bringt, unbemerkt eine Anfrage an eine andere Anwendung zu senden. Der Browser sendet dabei automatisch die gültigen Session-Cookies mit, sodass die Zielanwendung die Anfrage für legitim hält. Ein klassisches Beispiel: Ein Nutzer ist in einem Adminbereich eingeloggt, besucht in einem zweiten Tab eine präparierte Seite, und diese Seite löst per verstecktem Formular eine Passwortänderung oder eine Überweisung aus, ohne dass der Nutzer etwas davon merkt. Der CSRF-Schutz muss genau diesen Missbrauch verhindern.

Wer mit einem Framework wie Symfony arbeitet, bekommt CSRF-Schutz meist automatisch mitgeliefert, etwa über die Form-Komponente. Sobald aber ein eigenes, schlankes PHP-Projekt ohne Framework entsteht, zum Beispiel ein internes Tool, ein Legacy-System oder eine Microservice-Schnittstelle, fehlt dieser Schutz komplett, und Entwickler müssen ihn selbst bauen. Genau hier passieren die meisten Fehler: Tokens werden zu kurz generiert, mit unsicherer Zufallsquelle erzeugt oder mit einem einfachen String-Vergleich geprüft, der anfällig für Timing-Angriffe ist. Dieser Artikel zeigt, wie ein solider CSRF-Schutz ohne jede Framework-Abhängigkeit entsteht, Schritt für Schritt und mit vollständigem Code.

2. Das Synchronizer Token Pattern im Detail

Das Synchronizer Token Pattern ist die am weitesten verbreitete Methode für CSRF-Schutz und funktioniert nach einem einfachen Prinzip: Bei jedem Aufruf einer Seite mit einem Formular generiert der Server ein zufälliges Token, speichert es in der Session des Nutzers und bettet es als verstecktes Feld in das Formular ein. Sendet der Browser das Formular ab, muss das mitgeschickte Token exakt mit dem in der Session gespeicherten Wert übereinstimmen. Eine fremde Seite kennt dieses Token nicht, weil sie keinen Zugriff auf die Session des Opfers hat, und kann daher keine gültige Anfrage fälschen.

Der entscheidende Punkt für einen zuverlässigen CSRF-Schutz ist die Bindung des Tokens an die Server-Session, nicht an ein Cookie allein. Ein Angreifer kann zwar Cookies mitsenden lassen, aber er kann keinen serverseitigen Sitzungswert auslesen oder erraten, solange dieser mit ausreichender Entropie erzeugt wurde. Wichtig ist außerdem, dass das Token pro Session (nicht zwingend pro Formular) konsistent bleibt, damit mehrere gleichzeitig geöffnete Tabs nicht durch ständige Token-Rotation ungültig werden. Rotation nach erfolgreichem Login ist trotzdem sinnvoll, um Session-Fixation-Angriffe zusätzlich zu erschweren.

3. Sichere Token-Generierung mit random_bytes

Die Sicherheit des gesamten CSRF-Schutzes hängt an der Qualität der Zufallszahlen. PHP bietet mit random_bytes() seit Version 7 eine kryptographisch sichere Zufallsquelle, die auf Betriebssystemebene (/dev/urandom unter Linux, CryptGenRandom unter Windows) zugreift. Die veraltete Funktion mt_rand() ist hier tabu, da ihr Mersenne-Twister-Algorithmus vorhersagbar ist, sobald ein Angreifer genug Ausgaben beobachtet hat. Für ein CSRF-Token reichen 32 Byte Rohentropie, in Hexadezimaldarstellung also 64 Zeichen, vollkommen aus, um praktische Brute-Force-Angriffe auszuschließen.

Für den Vergleich zweier Tokens darf niemals der einfache Operator == oder === verwendet werden, weil String-Vergleiche in PHP intern zeichenweise abbrechen, sobald ein Unterschied gefunden wird. Diese frühzeitige Terminierung lässt sich über die Antwortzeit messen und ermöglicht theoretisch einen Timing-Angriff, der das Token Zeichen für Zeichen errät. Die Funktion hash_equals() vergleicht dagegen immer in konstanter Zeit, unabhängig davon, wie viele Zeichen übereinstimmen, und ist damit die einzig korrekte Wahl für sicherheitsrelevante String-Vergleiche wie den CSRF-Schutz.


<?php

declare(strict_types=1);

/**
 * Generates a cryptographically secure CSRF token.
 * Uses random_bytes() instead of mt_rand() or uniqid().
 */
function generate_csrf_token(): string
{
    // 32 bytes of raw entropy, encoded as a 64-character hex string
    return bin2hex(random_bytes(32));
}

/**
 * Compares two tokens in constant time to prevent timing attacks.
 * Never use == or === for security-sensitive comparisons.
 */
function csrf_tokens_match(string $submitted, string $stored): bool
{
    if ($submitted === '' || $stored === '') {
        return false;
    }

    return hash_equals($stored, $submitted);
}

4. Ein CSRF-TokenManager in der Praxis

Statt einzelne Funktionen über die Codebasis zu verstreuen, bündelt man den CSRF-Schutz am besten in einer kleinen, wiederverwendbaren Klasse. Diese Klasse kapselt die Session-Interaktion, die Token-Generierung und die Prüfung an einer Stelle, sodass jede Formularseite dieselbe, getestete Logik nutzt. Wichtig ist, dass die Session bereits gestartet ist, bevor die Klasse instanziiert wird, und dass ein sicheres Session-Cookie mit httponly und secure gesetzt wurde. Der TokenManager erzeugt bei Bedarf ein neues Token, gibt bei wiederholtem Aufruf aber dasselbe Token zurück, solange die Session gültig bleibt.

Die Prüfung erfolgt idealerweise zentral in einem Front-Controller oder Middleware-ähnlichen Einstiegspunkt, statt in jedem einzelnen Formular-Handler wiederholt zu werden. Dadurch verhindert man, dass ein einzelner vergessener Aufruf eine Sicherheitslücke öffnet. Für zustandsändernde Requests, also POST, PUT, PATCH und DELETE, sollte die Prüfung verpflichtend sein, für reine GET-Requests dagegen nicht, weil GET laut HTTP-Spezifikation keine Seiteneffekte auslösen sollte und ein CSRF-Schutz dort ohnehin ins Leere liefe.


<?php

declare(strict_types=1);

/**
 * Encapsulates CSRF token generation, storage and validation.
 * Requires an already started PHP session.
 */
final class CsrfTokenManager
{
    private const SESSION_KEY = '_csrf_token';

    public function __construct()
    {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            throw new RuntimeException('Session must be started before using CsrfTokenManager');
        }
    }

    /**
     * Returns the current token, generating one if none exists yet.
     */
    public function getToken(): string
    {
        if (empty($_SESSION[self::SESSION_KEY])) {
            $_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
        }

        return $_SESSION[self::SESSION_KEY];
    }

    /**
     * Validates a submitted token against the session value.
     */
    public function validate(?string $submittedToken): bool
    {
        $stored = $_SESSION[self::SESSION_KEY] ?? '';

        if ($stored === '' || $submittedToken === null || $submittedToken === '') {
            return false;
        }

        return hash_equals($stored, $submittedToken);
    }

    /**
     * Rotates the token, useful after login or privilege changes.
     */
    public function rotate(): string
    {
        $_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));

        return $_SESSION[self::SESSION_KEY];
    }
}

// Usage in a form handler
session_start();
$csrf = new CsrfTokenManager();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!$csrf->validate($_POST['csrf_token'] ?? null)) {
        http_response_code(403);
        exit('Invalid CSRF token');
    }
    // Process the trusted request here
}

5. Double-Submit-Cookie-Pattern für zustandslose APIs

Nicht jede Anwendung hat serverseitige Sessions, gerade bei zustandslosen REST-APIs mit JWT-Authentifizierung fehlt oft die klassische Session, an die ein Synchronizer-Token gebunden werden könnte. Für diesen Fall bietet sich das Double-Submit-Cookie-Pattern als Alternative für CSRF-Schutz an. Der Server setzt ein zufälliges Token als Cookie, das JavaScript im Frontend ausliest und bei jedem Request zusätzlich als Header mitsendet. Der Server vergleicht anschließend Cookie-Wert und Header-Wert, und nur bei Übereinstimmung gilt die Anfrage als legitim.

Die Sicherheit dieses Musters beruht auf der Same-Origin-Policy des Browsers: Eine fremde Seite kann zwar das Cookie automatisch mitsenden lassen, aber sie kann den Cookie-Wert nicht per JavaScript auslesen und daher nicht als Header duplizieren, sofern das Cookie nicht mit httponly markiert ist. Genau das ist die Besonderheit dieses CSRF-Schutzes: Das Token-Cookie darf hier ausnahmsweise nicht httponly sein, weil JavaScript darauf zugreifen muss. Diese Ausnahme wird oft übersehen und führt zu falsch konfigurierten Implementierungen, die den Schutz wirkungslos machen.


<?php

declare(strict_types=1);

/**
 * Double-submit cookie CSRF protection for stateless APIs.
 * The cookie is intentionally NOT httponly so JavaScript can read it.
 */
final class DoubleSubmitCsrf
{
    private const COOKIE_NAME = 'csrf_token';
    private const HEADER_NAME = 'X-CSRF-Token';

    public function issueCookie(): void
    {
        $token = bin2hex(random_bytes(32));

        setcookie(self::COOKIE_NAME, $token, [
            'expires' => 0,
            'path' => '/',
            'secure' => true,
            'httponly' => false, // must be readable by JavaScript
            'samesite' => 'Strict',
        ]);
    }

    public function validateRequest(): bool
    {
        $cookieToken = $_COOKIE[self::COOKIE_NAME] ?? '';
        $headerToken = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';

        if ($cookieToken === '' || $headerToken === '') {
            return false;
        }

        return hash_equals($cookieToken, $headerToken);
    }
}

6. SameSite-Cookies als zusätzliche Verteidigungsschicht

Das SameSite-Attribut von Cookies ist keine vollständige Lösung, verstärkt aber jeden CSRF-Schutz erheblich, wenn es korrekt gesetzt ist. Mit SameSite=Strict sendet der Browser das Session-Cookie ausschließlich bei Anfragen, die von der eigenen Domain stammen, selbst wenn ein Nutzer einen Link von einer fremden Seite anklickt. Mit SameSite=Lax, dem Standardwert moderner Browser, wird das Cookie bei einfachen Top-Level-Navigationen (normale Linkklicks) mitgesendet, aber nicht bei Formular-Submits per POST von fremden Seiten oder bei Bildern und iFrames.

Warum reicht SameSite allein trotzdem nicht als vollständiger CSRF-Schutz? Erstens unterstützen einige ältere Browser das Attribut nicht oder ignorieren es in bestimmten Konstellationen. Zweitens schützt SameSite=Lax ausdrücklich nicht vor allen Angriffsvektoren, etwa nicht vor Subdomain-Angriffen, wenn mehrere Anwendungen dieselbe übergeordnete Domain teilen. Drittens ist SameSite eine reine Browser-Funktion, auf die sich eine Anwendung nicht verlassen sollte, wenn sie Nutzer mit veralteten oder exotischen Clients bedienen muss. Die Kombination aus Synchronizer Token und SameSite=Strict ist deshalb deutlich robuster als jede einzelne Maßnahme.


<?php

declare(strict_types=1);

/**
 * Starts a session with hardened cookie parameters,
 * including SameSite as an additional CSRF defense layer.
 */
function start_hardened_session(): void
{
    session_set_cookie_params([
        'lifetime' => 0,
        'path' => '/',
        'domain' => '',
        'secure' => true,      // only sent over HTTPS
        'httponly' => true,    // not readable by JavaScript
        'samesite' => 'Strict', // not sent on cross-site requests
    ]);

    session_start();

    // Regenerate the session ID after login to prevent session fixation
    if (!isset($_SESSION['initialized'])) {
        session_regenerate_id(true);
        $_SESSION['initialized'] = true;
    }
}

7. CSRF-Schutz für Formulare und AJAX/Fetch kombinieren

Moderne PHP-Anwendungen bestehen selten nur aus klassischen HTML-Formularen, sondern kombinieren serverseitig gerenderte Seiten mit AJAX-Aufrufen über Fetch oder XMLHttpRequest. Der CSRF-Schutz muss beide Wege abdecken, ohne den Code zu duplizieren. Ein bewährtes Muster: Das Token wird einmal serverseitig in ein Meta-Tag im HTML-Head gerendert, und ein kleines JavaScript-Snippet liest diesen Wert aus und hängt ihn automatisch an jeden Fetch-Request als Header an, bevor die Anfrage abgeschickt wird.

Auf der PHP-Seite prüft ein zentraler Einstiegspunkt sowohl das POST-Feld als auch den Header, je nachdem, welcher Kanal für die jeweilige Anfrage verwendet wurde. Wichtig ist, den Header-Namen konsistent zu benennen, zum Beispiel X-CSRF-Token, und diesen in der serverseitigen Middleware genauso zu erwarten wie das Formularfeld csrf_token. Diese doppelte Prüfung verhindert Lücken, die entstehen, wenn ein Entwickler nur einen der beiden Wege bei der Migration von klassischen Formularen zu einer AJAX-lastigen Oberfläche berücksichtigt.

8. Typische Fehler bei eigener CSRF-Implementierung

Der häufigste Fehler bei selbstgebautem CSRF-Schutz ist die Verwendung von uniqid() oder mt_rand() zur Token-Generierung. Beide Funktionen sind für kryptographische Zwecke ungeeignet: uniqid() basiert auf der aktuellen Systemzeit in Mikrosekunden und ist damit in engen Grenzen vorhersagbar, mt_rand() nutzt einen nicht kryptographisch sicheren Pseudozufallsgenerator. Nur random_bytes() oder das darauf aufbauende random_int() liefern die nötige Entropie für einen belastbaren CSRF-Schutz.

Ein zweiter verbreiteter Fehler ist das Token als GET-Parameter in der URL zu übertragen. URLs landen in Server-Logs, Browser-Historien, Referrer-Headern und Proxy-Caches, wodurch das Token unbeabsichtigt an Dritte gelangen kann. Ein dritter Fehler betrifft die Prüfung selbst: Wird hash_equals() vergessen und stattdessen === verwendet, öffnet man theoretisch die Tür für Timing-Angriffe, auch wenn diese in der Praxis wegen Netzwerk-Jitter schwer auszunutzen sind. Ein vierter Fehler ist, den CSRF-Schutz nur im Frontend per JavaScript zu prüfen, ohne serverseitige Validierung, was jeden Schutz komplett wirkungslos macht, da ein Angreifer das Frontend schlicht umgeht.

9. CSRF-Schutzmuster im Vergleich

Je nach Architektur der Anwendung, session-basiert oder zustandslos, eignen sich unterschiedliche Ansätze für CSRF-Schutz besser oder schlechter. Die folgende Tabelle stellt die drei besprochenen Muster mit ihren jeweiligen Stärken gegenüber.

Muster Voraussetzung Stärke Schwäche
Synchronizer Token Serverseitige Session Höchste Sicherheit, gut erprobt Benötigt Session-Speicher
Double-Submit-Cookie JavaScript-Zugriff auf Cookie Funktioniert ohne Session Cookie darf nicht httponly sein
SameSite allein Moderner Browser Einfach zu aktivieren Kein vollständiger Schutz
Token + SameSite=Strict Session und moderner Browser Mehrschichtige Verteidigung Etwas mehr Implementierungsaufwand

In der Praxis empfiehlt sich für die meisten server-gerenderten PHP-Anwendungen das Synchronizer Token Pattern kombiniert mit SameSite=Strict. Für reine APIs ohne Session ist das Double-Submit-Cookie-Pattern die pragmatischere Wahl, solange die Ausnahme bei httponly bewusst dokumentiert wird. Beide Ansätze lassen sich in kleinen, testbaren PHP-Klassen kapseln, ohne dass ein komplettes Framework nötig wäre.

Mironsoft

PHP-Sicherheitsberatung und Audits für Individualsoftware

Eigener PHP-Code ohne verlässlichen CSRF-Schutz?

Wir prüfen bestehende PHP-Anwendungen auf CSRF-Lücken und implementieren robuste, getestete Schutzmechanismen, egal ob mit Session, JWT oder Hybrid-Architektur.

Security-Audit

Systematische Prüfung von Formularen und API-Endpunkten auf CSRF-Lücken

Implementierung

Token-Manager, Double-Submit-Cookies und SameSite-Konfiguration nach Maß

Legacy-Migration

Nachrüstung von CSRF-Schutz in gewachsenen PHP-Projekten ohne Framework

10. Zusammenfassung

Ein solider CSRF-Schutz ohne Framework lässt sich mit wenigen, klar abgegrenzten PHP-Bausteinen umsetzen: random_bytes() für die Token-Generierung, hash_equals() für den zeitkonstanten Vergleich, eine Session-gebundene Speicherung über das Synchronizer Token Pattern und, wo keine Session existiert, das Double-Submit-Cookie-Pattern als Alternative. SameSite=Strict ergänzt beide Ansätze als zusätzliche Verteidigungsschicht, ersetzt sie aber nicht vollständig, weil Browser-Unterstützung und Subdomain-Szenarien Lücken offenlassen können.

Wer diese Bausteine in einer kleinen, gut getesteten Klasse kapselt, statt sie über die Codebasis zu verstreuen, reduziert das Risiko vergessener Prüfungen erheblich. Zentrale Validierung in einem Front-Controller, konsequente Nutzung von hash_equals() und der Verzicht auf Tokens in URLs sind die drei Punkte, die den größten Unterschied zwischen einem echten und einem nur scheinbaren CSRF-Schutz ausmachen.

CSRF-Schutz ohne Framework — Das Wichtigste auf einen Blick

Token-Generierung

random_bytes(32) statt mt_rand() oder uniqid() für kryptographisch sichere Tokens.

Vergleich

hash_equals() statt ===, um Timing-Angriffe beim Token-Vergleich auszuschließen.

Ohne Session

Double-Submit-Cookie-Pattern für zustandslose APIs, Cookie bewusst nicht httponly.

Zusätzliche Schicht

SameSite=Strict ergänzt, ersetzt aber nicht das Token-Pattern.

11. FAQ: CSRF-Schutz ohne Framework

1Was ist CSRF und warum betrifft es kleine Projekte?
CSRF nutzt automatisch mitgesendete Session-Cookies aus. Jede session-basierte Anwendung ist betroffen, egal wie klein.
2Warum random_bytes statt mt_rand?
random_bytes ist kryptographisch sicher, mt_rand ist vorhersagbar nach genug beobachteten Ausgaben.
3Reicht SameSite=Strict allein?
Nein, es ist eine wertvolle Zusatzschicht, ersetzt aber nicht das Token-Pattern wegen Browser- und Subdomain-Lücken.
4Warum darf das Cookie nicht httponly sein?
Beim Double-Submit-Pattern muss JavaScript den Cookie-Wert lesen können, um ihn als Header zu duplizieren.
5Warum kein Token in der URL?
URLs landen in Logs, Historien und Referrer-Headern. Tokens gehören in POST-Body oder Header.
6Braucht jedes Formular ein eigenes Token?
Nicht zwingend, ein Token pro Session reicht meist. Rotation nach Login erhöht die Sicherheit.
7Muss GET auch geprüft werden?
Nein, GET sollte laut Spezifikation keine Seiteneffekte haben. Prüfung gehört auf POST, PUT, PATCH, DELETE.
8Wie funktioniert es bei AJAX-Oberflächen?
Token per Meta-Tag rendern, per JavaScript auslesen und bei jedem Fetch als Header mitsenden, zentral serverseitig prüfen.
9Was passiert ohne hash_equals?
Theoretisches Timing-Angriffsrisiko durch frühzeitigen String-Vergleichsabbruch, in der Praxis schwer, aber vermeidbar.
10Passt das Muster zu Single-Page-Apps?
Ja bei gemeinsamer Session mit dem Backend, sonst ist das Double-Submit-Cookie-Pattern die passendere Wahl.