Cybersicherheit

CSRF-Schutz für kleine PHP-Anwendungen - Mehr als nur ein Session-Cookie verlangen

CSRF-Schutz für kleine PHP-Anwendungen - Mehr als nur ein Session-Cookie verlangen

Ein Einstellungsformular kann eine gültige Anmeldung verlangen und trotzdem eine Anfrage akzeptieren, die der Benutzer nie absenden wollte. Das Session-Cookie teilt dem Server mit, welche Browser-Session die Anfrage stellt, aber nicht unbedingt, welche Seite den Browser zum Absenden veranlasst hat. Wie kann eine kleine PHP-Anwendung ihr eigenes Formular von einer gefälschten Anfrage unterscheiden?

Dies ist das Problem hinter Cross-Site Request Forgery, meist als CSRF abgekürzt. Es ist besonders relevant, wenn ein Browser Zugangsdaten wie ein PHP-Session-Cookie automatisch mitsendet und ein Endpunkt etwas verändert: eine E-Mail-Adresse, ein Passwort, einen Beitrag, eine Berechtigung oder eine Abrechnungseinstellung. Das Ziel ist enger gefasst als „Websicherheit lösen“. Zustandsverändernde Anfragen sollen einen ausdrücklichen Nachweis enthalten und von Kontrollen umgeben sein, die sicher fehlschlagen.

Das Cookie authentifiziert eine Session, nicht die Herkunft der Anfrage

Angenommen, ein Benutzer ist bei account.example angemeldet. In einem anderen Tab sendet eine fremd kontrollierte Seite ein Formular an https://account.example/profile/email. Browser erlauben seit Langem, HTML-Formulare über Origin-Grenzen hinweg abzusenden. Je nach Cookie-Attributen und Kontext der Anfrage kann der Browser das Session-Cookie des Benutzers mitsenden. Prüft der empfangende Endpunkt nur dieses Cookie und vorhersagbare Formularfelder, kann er die gefälschte Anfrage mit einer beabsichtigten verwechseln.

MDN beschreibt drei Bedingungen für den klassischen Fall: Die Anfrage verändert den Zustand des Servers, Cookies sind der einzige Nachweis zur Identifikation des Benutzers und die übrigen Anfrageparameter sind vorhersagbar. Der Angreifer muss die Antwort nicht lesen können, damit die unerwünschte Aktion Schaden anrichtet.

Dieser Unterschied wird leicht übersehen, weil die Authentifizierung tatsächlich funktioniert hat. Die Anwendung hat die Session korrekt erkannt und anschließend die falsche Frage beantwortet. „Wer ist angemeldet?“ ist nicht dasselbe wie „Stammt diese Anfrage aus einem Ablauf, den unsere Anwendung ausgegeben hat?“

Zuerst alle zustandsverändernden Routen erfassen

Ein Token-Helper schützt nichts, wenn ein vergessener Endpunkt ihn umgeht. Am Anfang sollte eine Liste aller Routen stehen, die Daten erstellen, aktualisieren, löschen, veröffentlichen oder hochladen, Einladungen senden, An- und Abmeldungen durchführen oder andere Nebenwirkungen auslösen. Dazu gehören alte Handler, Sammelaktionen und JavaScript-Endpunkte, nicht nur sichtbare Formulare.

Zustandsänderungen sollten sich nicht hinter GET verbergen. RFC 9110 definiert GET, HEAD, OPTIONS und TRACE als sichere Methoden, deren festgelegte Semantik im Wesentlichen nur lesend ist. Eine URL wie /post/delete?id=42 wird nicht dadurch sicher, dass sie als Aktionslink bezeichnet wird. Crawler, Vorschauen, Prefetcher und websiteübergreifende Elemente können URLs aufrufen. Für eine Änderung ist eine unsichere Methode wie POST zu verwenden, gefolgt von einer serverseitigen Prüfung des CSRF-Schutzes.

POST ist für sich genommen kein Schutz. Eine bösartige Seite kann ein gewöhnliches originübergreifendes HTML-Formular per POST absenden. Die Methode drückt die Art des Vorgangs aus; ein weiterer Mechanismus muss belegen, dass die Anfrage zu einem legitimen Ablauf der Anwendung gehört.

Ein Synchronizer-Token für sessionbasierte Formulare verwenden

Für eine zustandsbehaftete Anwendung empfiehlt das OWASP CSRF Prevention Cheat Sheet das Synchronizer-Token-Muster. Der Server erzeugt einen geheimen, nicht vorhersagbaren Wert, speichert ihn in der Session des Benutzers und fügt dem legitimen Formular eine Kopie hinzu. Beim Absenden verlangt die Anwendung, dass beide Werte übereinstimmen.

Das Session-Cookie wird nach den Cookie-Regeln des Browsers übertragen. Das versteckte Token wird übertragen, weil die Anwendung es bewusst in die Seite eingefügt hat. Eine andere Website kann versuchen, denselben Feldnamen zu senden, doch die Same-Origin Policy hindert sie normalerweise daran, die legitime Seite zu lesen und den Wert zu erfahren. Das Token liefert somit einen zusätzlichen Nachweis, dass die Anfrage Inhalte durchlaufen hat, die für diese Session erzeugt wurden.

Token erzeugen und ausgeben

Das folgende native PHP-Beispiel verwendet ein Token pro Session. Es setzt voraus, dass eine sichere Session-Konfiguration und session_start() bereits in einem gemeinsamen Bootstrap ausgeführt wurden.

function csrfToken(): string
{
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }

    return $_SESSION['csrf_token'];
}

$token = htmlspecialchars(
    csrfToken(),
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

Das PHP-Handbuch dokumentiert random_bytes() als Quelle kryptografisch sicherer Zufallsbytes. Rohe Bytes können Zeichen enthalten, die sich nicht zur direkten Ausgabe eignen. bin2hex() wandelt sie daher in eine transportfreundliche Zeichenfolge um. Zweiunddreißig Bytes sind hier ein sinnvolles Beispiel, keine universell vorgeschriebene Anwendungsnorm.

Der codierte Wert wird in jedem geschützten Formular ausgegeben:

<form method="post" action="/profile/email">
    <input type="hidden" name="csrf_token" value="<?= $token ?>">
    <input type="email" name="email" required>
    <button type="submit">Update email</button>
</form>

Output-Encoding bleibt sinnvoll, obwohl der Wert vom Server erzeugt wurde. Es macht die HTML-Grenze ausdrücklich sichtbar und verhindert, dass eine spätere Änderung der Token-Darstellung unbemerkt ein Markup-Problem erzeugt.

Vor Ausführung der Aktion ablehnen

Die Validierung gehört vor Datenbankschreibvorgänge, E-Mail-Versand und jede andere Nebenwirkung:

function requireValidCsrfToken(): void
{
    $known = $_SESSION['csrf_token'] ?? null;
    $submitted = $_POST['csrf_token'] ?? null;

    if (!is_string($known)
        || !is_string($submitted)
        || !hash_equals($known, $submitted)
    ) {
        http_response_code(403);
        exit('Invalid request token');
    }
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Allow: POST');
    http_response_code(405);
    exit('Method not allowed');
}

requireValidCsrfToken();

// Authorize the user, validate the new email, then update it.

Die Typprüfungen sorgen dafür, dass fehlende Werte sicher abgelehnt werden und unerwartete Array-Eingaben keine String-Funktion erreichen. Das PHP-Handbuch beschreibt hash_equals() als timing-sicheren Vergleich und verlangt den bekannten Serverwert als erstes sowie den vom Benutzer gelieferten Wert als zweites Argument. Bei fehlgeschlagener Validierung darf keines der Tokens protokolliert werden.

Ein gültiges Token ist weiterhin keine Autorisierung. Der Handler muss separat prüfen, ob dieser Benutzer dieses Profil bearbeiten darf und ob die übermittelten Daten zulässig sind. Die CSRF-Validierung beantwortet eine Frage zur Integrität der Anfrage; sie darf nicht als Abkürzung an der Zugriffskontrolle vorbei dienen.

Den Lebenszyklus des Tokens bewusst wählen

OWASP sieht Tokens pro Anfrage oder pro Session vor. Eine Rotation pro Anfrage begrenzt die Wiederverwendung, kann jedoch Abläufe mit der Zurück-Schaltfläche und mehreren geöffneten Tabs beeinträchtigen. Ein Token pro Session ist für eine kleine Anwendung einfacher und bleibt nützlich, wenn es geheim und nicht vorhersagbar ist, konsequent geprüft und zusammen mit der Vertrauensgrenze der Session ersetzt wird.

Nach der Anmeldung oder einem anderen wichtigen Session-Übergang sollte ein neues Token erzeugt werden, insbesondere wenn zuvor bereits eine anonyme Session bestand. Beim Zerstören der Session wird es entfernt. Kann ein Benutzer mehrere unabhängige Login-Sessions besitzen, sollte jede Session einen eigenen Wert haben. So folgt das Token der authentifizierten Session und nicht einem dauerhaften Benutzerdatensatz.

Ein Synchronizer-Token gehört nicht in eine URL. URLs können im Browserverlauf, in Zugriffsprotokollen, Analytics-Systemen, kopierten Nachrichten und Referrer-Informationen erscheinen. Je nach Fall sind ein verstecktes Formularfeld, ein JSON-Body oder ein eigener Request-Header zu verwenden. Ein verstecktes Feld soll nicht vor dem Benutzer unsichtbar sein; seine Sicherheitseigenschaft besteht darin, dass ein fremder Origin die geschützte Seite normalerweise nicht lesen kann.

Seiten mit Tokens benötigen außerdem eine überlegte Cache-Richtlinie. Ein gemeinsam genutzter Cache darf das tokenhaltige Formular eines Benutzers nicht an einen anderen ausliefern. Das gehört allgemein zum Schutz authentifizierter Seiten, doch CSRF-Tokens machen die Folge besonders deutlich.

Cookie- und Request-Kontext-Schutz ergänzen, ohne einem einzelnen Flag zu vertrauen

Ein Session-Cookie sollte normalerweise Secure, HttpOnly, einen engen Host-/Pfadbereich und einen bewusst gewählten SameSite-Wert verwenden. MDN bezeichnet SameSite=Lax oder Strict als partiellen CSRF-Schutz. Strict verhindert das Mitsenden von Cookies in mehr websiteübergreifenden Situationen, kann aber legitime Einstiege von einer anderen Website stören; Lax erhält mehr Navigationsverhalten und ist schwächer.

Same-site ist zudem weiter gefasst als same-origin. Geschwister-Subdomains können same-site sein, obwohl sie unterschiedliche Origins besitzen. Ein Design, das allen Subdomains gleichermaßen vertraut, sollte dies ausdrücklich festhalten. SameSite bleibt Defense in Depth; Token-Prüfungen sollten nicht entfernt werden, nur weil ein Cookie ein vielversprechendes Attribut trägt.

Moderne Browser können Fetch-Metadata-Header senden. Insbesondere beschreibt Sec-Fetch-Site eine Anfrage als same-origin, same-site, cross-site oder none. Ein Server kann eine als cross-site markierte unsichere Anfrage ablehnen, bevor sie die Geschäftslogik erreicht. Das zitierte W3C-Dokument zu Fetch Metadata ist jedoch ein Working Draft. Reale Anwendungen können auf ältere Clients, Vermittler, beabsichtigte originübergreifende Abläufe oder fehlende Header treffen. Eine solche Richtlinie sollte mit Beobachtung, ausdrücklichen Ausnahmen und einem Fallback eingeführt werden, statt die ständige Anwesenheit des Headers vorauszusetzen.

Formulare, JSON-APIs und Webhooks folgen nicht derselben Regel

Für JavaScript-Clients kann das Token in einem eigenen Header wie X-CSRF-Token übertragen werden. Eigene Header und nicht einfache Content-Types veranlassen Browser zu Same-Origin- und CORS-Prüfungen, was hilfreich sein kann. Ein zu großzügiges credentialed CORS kann diese Grenze jedoch wieder öffnen. „Es verwendet JSON“ ist daher kein vollständiges Sicherheitsargument.

Nicht jeder POST-Endpunkt sollte ein browser-sessionbasiertes CSRF-Token verwenden. Ein Webhook stammt von einem anderen Server und benötigt eine eigene Authentifizierung, häufig eine vom Anbieter definierte Signatur. Eine öffentliche API mit einem ausdrücklichen Bearer-Credential, das Browser nicht automatisch anhängen, hat ein anderes Bedrohungsmodell. Umgekehrt benötigt eine API mit Cookie-Authentifizierung auch dann ein CSRF-Konzept, wenn ihre Antwort JSON enthält. Entscheidend ist, wie Credentials übertragen werden, nicht die Dateiendung oder das Framework-Etikett.

Abgelehnte Pfade testen, nicht nur den Erfolgsfall

Ein sicherer Testplan kann das Verhalten prüfen, ohne fremde Systeme anzugreifen:

  1. Ein geschütztes Formular mit seinem gültigen Session-Token absenden und bestätigen, dass die beabsichtigte Aktion erfolgreich ist.
  2. Das Token entfernen, durch einen Zufallswert ersetzen und statt eines Strings ein Array senden; jede Anfrage muss vor einer Nebenwirkung scheitern.
  3. GET an jede zustandsverändernde Route senden und bestätigen, dass ein Methodenfehler zurückgegeben wird, ohne Daten zu verändern.
  4. Abmelden und ein Token aus der beendeten Test-Session erneut senden; es darf nicht mehr akzeptiert werden.
  5. Zwei Formulare in getrennten Tabs öffnen und prüfen, ob sich die gewählte Rotationsrichtlinie wie dokumentiert verhält.
  6. Response-Cookies untersuchen und bestätigen, dass Secure, HttpOnly, SameSite, Host und Pfad dem Design entsprechen.
  7. Bei aktiviertem Fetch-Metadata-Filter zunächst legitime Clients beobachten und jede dokumentierte originübergreifende Ausnahme testen.

Protokolliert werden können Route, Ergebnis und ein nicht sensibler Grund wie „missing token“ oder „mismatch“, aber nicht das Token selbst. Eine Ablehnung kann ein Angriff, eine abgelaufene Seite, ein alter Tab oder ein Implementierungsfehler sein. Logs unterstützen die Untersuchung; sie beweisen für sich genommen keine Absicht.

Verstehen, was CSRF-Schutz nicht behebt

Cross-Site Scripting kann CSRF-Schutzmaßnahmen untergraben, weil ein Skript im vertrauenswürdigen Origin möglicherweise ein Token lesen oder Anfragen im Kontext der Anwendung senden kann. Token-Validierung ersetzt daher weder Output-Encoding noch eine solide Content Security Policy oder andere XSS-Schutzmaßnahmen. Sie behebt auch keine fehlerhafte Autorisierung, gestohlene Sessions, unsichere Passwortwiederherstellung oder Vorgänge, die eine erneute Authentifizierung verlangen sollten.

Für eine besonders folgenreiche Aktion kann die Eingabe des aktuellen Passworts, eine WebAuthn-Assertion oder eine andere ausdrückliche Benutzerinteraktion angemessen sein. Die richtige Schwelle hängt vom möglichen Schaden der Aktion ab. Eine Blogeinstellung und das Löschen eines Kontos verdienen nicht dieselbe Reibung.

Zwei unabhängige Nachweise verlangen

Eine kleine PHP-Anwendung muss nicht behaupten, dass ein einzelnes Token jede Anfrage vertrauenswürdig macht. Sie benötigt eine zusammenhängende Grenze: Unsichere Aktionen verwenden kein GET, session-authentifizierte Formulare tragen ein nicht vorhersagbares serverseitiges Token, Handler lehnen fehlende oder abweichende Werte vor Nebenwirkungen ab und anschließend erfolgt weiterhin die Autorisierung.

Cookie-Einschränkungen und Fetch Metadata können diese Grenze stärken, doch ihre Einschränkungen müssen sichtbar bleiben. Die nützliche Frage lautet nicht „Haben wir CSRF-Schutz aktiviert?“, sondern „Kann jede zustandsverändernde Route erklären, welchen unabhängigen Nachweis sie verlangt, und haben wir die Anfrage ohne diesen Nachweis getestet?“

References