1. CSRF: Daten und security-state
Wenn wir kurz von den Begriffen absehen und die Anwendung mit gesundem Menschenverstand betrachten, sehen wir: Das System lebt in Zuständen. Der Benutzer hat „eingeloggt/nicht eingeloggt“, der Entwurf hat „DRAFT/SUBMITTED“, das Profil hat „alter Wert/neuer Wert“. CSRF wählt keine „liebgewonnenen“ Entitäten aus, ihm ist der Fakt an sich wichtig: Die Anfrage ändert den Zustand von etwas Bedeutsamem.
An dieser Stelle ist es sehr hilfreich, die zwei Arten von „Zuständen“, die durch Anfragen geändert werden, klar zu trennen. Der Geschäftszustand (Business-State) sind Ihre Domain-Daten: Profil, Entwürfe, Inhaltsstatus. Der Security-State ist das, als wer der Browser in der aktuellen Sitzung gilt: als Anonymer oder als konkreter Benutzer und welchen Rollensatz er hat. Und Login/Logout sind genau die Schalter des Security-State.
Halten wir das in einer kleinen Tabelle fest – ohne Philosophie, maximal anwendungsorientiert:
| Operation | Was sich tatsächlich ändert | Warum ist es ein CSRF-Thema |
|---|---|---|
| PATCH /api/me/profile | Benutzerprofil (Geschäftszustand) | Der Browser kann die Anfrage mit einem Cookie „selbst“ senden; es muss bestätigt werden, dass die Aktion von Ihrer UI initiiert wurde. |
| DELETE /api/drafts/{id} | Daten (der Entwurf verschwindet) | Dasselbe: „Löschen“ ist eine starke Zustandsänderung. |
| POST /login (in formLogin) | Security-State: Die Sitzung wird authentifiziert. | Wenn ein Angreifer den Login „unterschiebt“, wird das Opfer falsch (unerwünscht) eingeloggt. |
| POST /logout | Security-State: Die Sitzung wird nicht mehr authentifiziert. | Wenn der Logout gefälscht werden kann, kann der Benutzer ohne seine Absicht aus dem System „geworfen“ werden. |
Das heißt, der CSRF-Schutz dreht sich nicht um „Daten vs. Nicht-Daten“. Er dreht sich um „ändert Zustand vs. ändert nicht“. Erinnern Sie sich an die Formulierung aus der ersten Vorlesung: Die Verwundbarkeit lebt dort, wo der Browser automatisch den Authentifizierungskontext anfügt. Und Login/Logout ändern genau, welcher Kontext in Zukunft angefügt wird.
2. Login-CSRF und Änderung des security-state
Wenn wir „Login“ sagen, malt das Gehirn automatisch ein Bild: Der Benutzer gibt selbst Login/Passwort ein, klickt auf „Anmelden“, das System prüft – und fertig. Aus Sicht des Angreifers ist jedoch eine andere Frage interessanter: Kann man den Browser des Opfers dazu bringen, einen Login ohne dessen Absicht auszuführen? Das ist das typische Szenario des „Login-CSRF“ (manchmal auch „Forced Login“ genannt).
Die Gefahr besteht darin, dass der Login nicht nur irgendein Flag ändert. Er ändert die Antwort auf die Frage „Wer bist du?“ im Rahmen der aktuellen Sitzung. Und dann arbeiten alle Ihre geschützten Endpunkte „im Namen“ dessen, was die Sitzung geworden ist. Wenn der Angreifer den Login auf seinen eigenen Account unterschieben konnte, könnte das Opfer in der System als „jemand anders“ handeln, ohne die Vertauschung zu bemerken, besonders wenn die UI den Benutzernamen nicht sehr deutlich anzeigt.
Stellen wir uns eine böse, aber lebensnahe Geschichte vor. Ein Benutzer öffnet eine fremde Website (etwa „10 Möglichkeiten, Java um das 200-fache zu beschleunigen“, wir sind alle schon mal darauf hereingefallen). Diese Website schiebt im Browser eine Anfrage auf POST /login unter, deren Formulardaten den Login/Passwort des Angreifer-Accounts enthalten. Wenn in Ihrer Anwendung der Login-Endpunkt nicht durch einen CSRF-Mechanismus geschützt ist, kann der Server die Sitzung authentifizieren und sie an den Account des Angreifers „binden“. Und dann geht der Benutzer ahnungslos zu Ihrer Secure Content Platform API, erstellt einen Entwurf, schreibt etwas Persönliches hinein … nur schreibt er es in den Account des Angreifers. Herzlichen Glückwunsch: „Datendiebstahl“ ohne Diebstahl des Passworts des Opfers.
Zeichnen wir dieses Szenario in einem Diagramm. Es ist ein wenig theatralisch, aber nützlich – es hilft, die Mechanik mit den Augen zu sehen:
sequenceDiagram
%% Idee: Der Angreifer initiiert einen Login auf seinen Account über den Browser des Opfers
%% Schlüsselmoment: Der Browser fügt automatisch Cookies hinzu (falls vorhanden)
participant V as "Opfer (Browser)"
participant E as "Bösewicht-Seite"
participant A as "Secure Content Platform API"
V->>E: Öffnet die Seite des Angreifers
E->>V: "Schiebt ein Auto-Submit des Formulars auf /login unter"
V->>A: "POST /login (Login/Passwort des Angreifers) + Cookie, falls vorhanden"
A->>A: "Authentifizierung, Befüllen des SecurityContext, session becomes authenticated"
A-->>V: "302/200 (abhängig vom Flow)"
V->>A: "Danach ruft das Opfer /api/me, /api/drafts... auf"
A->>A: "Der Server geht davon aus, dass es der Account des Angreifers ist"
Und hier ist die wichtigste Erkenntnis der Vorlesung: Login ist eine ändernde Operation, weil sie die „Security-Identität“ der aktuellen Sitzung ändert. Daher muss nicht nur „geschäftliches PATCH/DELETE“ mit einem CSRF-Token geschützt werden, sondern auch der Login selbst.
Verbindung zu Spring Security und formLogin
Die gute Nachricht: Wenn Sie das standardmäßige formLogin verwenden und die Einstellungen nicht „frontal“ umgehen, behandelt Spring Security den Login bereits standardmäßig als state-changing-Operation. Die Standard-Login-Seite, die Spring ausliefert, sendet beispielsweise einen POST und enthält einen CSRF-Token (als verstecktes Formularfeld). Wichtig ist jetzt die Tatsache: Sogar der Login erfordert einen CSRF-Token, weil er den Sicherheitszustand ändert.
In unserer Anwendung enthält SecurityFilterChain bereits formLogin und logout. In minimaler Form sieht das so aus (Ausschnitt, den Sie in die vorhandene Konfiguration einfügen):
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Standardmäßiges formLogin einbinden: Die Verarbeitung von POST /login wird Teil der Sicherheitsschicht sein.
http.formLogin(form -> {});
// Logout ändert ebenfalls den security-state (Session-Invalidierung/Löschung des Sicherheitskontexts)
http.logout(logout -> {});
// Die Filterkette von Spring Security zusammenbauen
return http.build();
}
Das ist ein einfacher, aber wichtiger Gedanke: Login/Logout leben nicht in „Business-Controllern“, sondern in der Sicherheitsschicht, weil sie genau den security-state ändern. Daher befindet sich auch die CSRF-Logik dort.
3. Logout: Abmeldung per GET ist nicht zulässig
Die Situation beim Logout ist noch aussagekräftiger, da ein Anfänger ihn oft als „harmlosen Knopf“ betrachtet: Man loggt sich aus, man loggt sich aus, es wird nichts gelöscht. Tatsächlich ist der Logout eine Operation, die den Sicherheitszustand des Systems ändert: Sie werfen den Benutzer aus dem authentifizierten Zustand und löschen/invalidieren das, was ihn „eingeloggt“ gemacht hat (meistens die Session oder den SecurityContext in der Session).
Wenn der Logout gefälscht werden kann, erhält der Angreifer die Möglichkeit, den „Hebel“ einer fremden Session umzulegen. Dies mag kein so dramatisches Risiko wie die Löschung von Daten sein, aber es ist durchaus real: Ständige Forced-Logouts können die Workflows des Benutzers stören, ihn daran hindern, sein Profil zu bearbeiten, Entwürfe zu erstellen, wichtige Aktionen durchzuführen. Und manchmal (in komplexen Systemen) kann Forced-Logout Teil einer größeren Attacke sein: Aus dem System geworfen → erneuter Login in den „falschen“ Account untergeschoben → das Opfer arbeitet im falschen Kontext weiter.
Der Schlüsselmoment, den Sie sich als Faustregel merken sollten: Wenn CSRF aktiviert ist, führt Spring Security standardmäßig den Logout per POST und nicht per GET durch. Warum? Weil GET eine „safe method“ sein sollte (in idealer Architektur reines Lesen), und Logout eine Zustandsänderung darstellt. Einen Logout per GET durchzuführen ist, als würde man einen „Abmelden“-Button in ein Banner hängen: Jeder kann „für Sie klicken“, indem er einfach den Browser zwingt, die URL zu laden.
Stellen Sie sich eine schlechte Welt vor, in der der Logout per GET /logout verfügbar ist. Dann braucht der Angreifer nicht einmal JavaScript. Es reicht, ein Bild auf der Seite einzufügen:
<!-- SCHLECHT: Wenn /logout per GET verfügbar ist, zieht der Browser es sofort beim Laden des Bildes -->
<img src="https://ihre-seite/logout">
Der Browser lädt aus „Höflichkeit“ dieses Bild, sendet automatisch das Cookie Ihrer Session mit – und schon ist der Benutzer ausgeloggt. Das Bild wird natürlich nicht geladen (weil die URL kein Bild ist), aber der Logout ist bereits erfolgt, und der Browser ist nicht sauer. Er ist selten sauer, er funktioniert einfach.
Spring Security versucht, Sie vor solcher „harmlosen“ Selbsttäuschung zu schützen. Daher ist in der normalen Konfiguration, selbst wenn Sie nichts angepasst haben, der Logout ein POST /logout und steht unter CSRF-Schutz.
Antipattern: „Lassen wir Logout per GET zu, das ist bequemer“
Manchmal sieht ein Entwickler, dass der Logout nicht per „Link“ funktioniert (oder im Browser durch direkten Aufruf von /logout), und trifft eine Entscheidung auf der Ebene „damit es funktioniert“: Er erlaubt Logout per GET. Technisch können Sie das tun, Spring erlaubt es, aber gerade in Bezug auf CSRF ist dies ein gutes Beispiel dafür, wie man es nicht machen sollte.
Hier sehen Sie einen solchen „Shortcut“ (ich zeige ihn als Antibeispiel – nicht als Empfehlung):
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
// SCHLECHT: Logout per GET macht Logout zu einem CSRF-Ziel
http.logout(logout -> logout
// Hier sagen wir Spring Security explizit: "Behandle GET /logout als Logout-Anfrage"
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
);
Diese Zeile macht den Logout zu einem „Link“. Aber gleichzeitig macht sie den Logout zu einem leichten Ziel für Anfragenfälschung. Im Übungsprojekt trainieren wir genau, solche Entscheidungen nicht als „Juhu, es funktioniert“, sondern als „Was habe ich gerade im Sicherheitsmodell kaputt gemacht?“ zu sehen.
4. CSRF vs. Rollen: unterschiedliche Prüfungen
Eine sehr häufige Verwirrung bei Anfängern sieht so aus: „Wenn mein Endpunkt authenticated() verlangt, wozu noch CSRF? Der Angreifer ist doch nicht eingeloggt!“ Und hier muss man sich die Schlüsselidee ins Gedächtnis rufen: CSRF funktioniert genau dann, wenn der Benutzer bereits eingeloggt ist und der Browser selbst das Session-Cookie anfügt.
Rollen-/Autorisierungsprüfungen beantworten die Frage „Darf dieser Benutzer das?“. Und CSRF beantwortet die Frage „Wurde diese Aktion durch ein korrektes Szenario initiiert?“. Es sind zwei unabhängige Prüfungen, und beide können unabhängig voneinander wahr/falsch sein.
Stellen Sie sich eine Anfrage PATCH /api/me/profile vor. Sie sollte dem eingeloggten Benutzer erlaubt sein (authorization), aber sie muss auch von „unserer UI“ initiiert worden sein (CSRF). Wenn CSRF ausgeschaltet ist, kann der Angreifer die Anfrage im Namen des eingeloggten Benutzers initiieren, und die Authorization sagt ehrlich „Ja, das ist USER, er darf das Profil ändern“. Spring Security ist kein Hellseher: Wenn Sie CSRF nicht aktiviert haben, erfährt es nicht, dass die Anfrage von einer fremden Seite kam.
Deshalb sehen Sie im SecurityFilterChain normalerweise sowohl Zugriffsregeln als auch den CSRF-Schutz. Etwa in diesem Stil (stark vereinfacht, um die Idee zu zeigen):
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// Authorization: "wer darf" (Rechte-/Authentifizierungsprüfung)
.requestMatchers(HttpMethod.PATCH, "/api/me/profile").authenticated()
.anyRequest().permitAll()
);
// Wichtig: CSRF ist eine separate Prüfung, sie lebt in den Filtern und wird separat konfiguriert
return http.build();
}
Ja, hier schreiben wir nicht explizit „csrf“ – noch nicht. Aber der Gedanke ist: Authorization gibt vor, „wer darf“, CSRF gibt vor, „wie eine korrekte Anfrage im Browsermodell aussehen muss“. Und Login/Logout sind genau solche state-changing-Operationen – nur ändern sie nicht das Profil, sondern den security-state.
5. Wo das im Projekt sichtbar ist
Im Projekt ist es nützlich, eine Regel im Kopf zu behalten: Im session-basierten Zweig bezieht sich CSRF auf alle unsicheren Methoden, die entweder den Geschäftszustand oder den security-state ändern. Profil, Entwürfe und Submit-Szenarien unterliegen derselben Logik wie jedes andere PATCH / POST / DELETE: Das Cookie zeigt, dass der Browser eine Session hat, beweist aber nicht, dass die Aktion wirklich von Ihrer Oberfläche initiiert wurde.
Für die aktuelle Vorlesung sind zwei Punkte besonders wichtig, die leicht unterschätzt werden:
– POST /login – ändert den security-state: Die Sitzung wird authentifiziert.
– POST /logout – ändert ebenfalls den security-state: Die Session wird gelöscht, und der Browser gilt nicht mehr als eingeloggter Benutzer.
Das hilft, nicht in der falschen Vorstellung stecken zu bleiben, dass CSRF nur für PATCH des Profils und DELETE des Entwurfs relevant ist. Es taucht überall auf, wo der Browser mit einer Cookie-Session den Zustand ändert – nur dass es in manchen Fällen Domain-Daten und in anderen das eigene Sicherheitsbild des Benutzers sind.
6. Typische Fehler bei CSRF Login/Logout
Fehler Nr. 1: „CSRF betrifft nur Geschäftsdaten, Login/Logout haben nichts damit zu tun“.
Ein solches Denken entsteht, wenn wir den Systemzustand nur als Tabellen in der Datenbank betrachten und den security-state als „unwichtigen Servicekram“ wahrnehmen. In der Praxis ist der security-state genauso ein Systemzustand wie der Status eines Entwurfs. Login und Logout ändern diesen Zustand, und ein Angreifer hat oft ein Interesse daran, genau diese Schalter zu betätigen, um das Verhalten des Benutzers zu steuern.
Fehler Nr. 2: „Logout ist harmlos, soll GET sein, das ist bequemer“.
Bequemlichkeit im Browser ist oft trügerisch. Logout per GET wird zu einer „Link-Operation“, die leicht von einer fremden Seite initiiert werden kann. Ja, das ist nicht die Löschung aller Daten, aber es ist ein realer Hebel: Der Benutzer kann aus dem System geworfen werden, was seine Workflows stört. Im Übungsprojekt trainieren wir, uns für Sicherheit und Vorhersagbarkeit zu entscheiden, nicht für „damit es klickbar ist“.
Fehler Nr. 3: „Wenn der Endpunkt durch Rollen geschützt ist, wird CSRF nicht benötigt“.
Rollen und CSRF lösen unterschiedliche Aufgaben. Rollen sagen „dieser Benutzer hat das Recht“, CSRF sagt „diese Aktion stammt aus einem korrekten UI-Szenario“. Bei einem CSRF-Angriff hat der Benutzer bereits die Rolle und ist bereits eingeloggt – sonst funktioniert der Angriff normalerweise nicht. Daher schützt die Rolle nicht vor CSRF, sie existiert einfach parallel.
Fehler Nr. 4: „Wenn ich CSRF nicht im Controller-Code sehe, dann gibt es keins“.
Das ist ein echter Klassiker von Spring Security: Die CSRF-Prüfung erfolgt auf der Ebene der Filter, bevor der Controller erreicht wird. Daher ist das Fehlen einer „manuellen Token-Prüfung“ in der Controller-Methode genau das normale Zeichen: Sie haben den Schutz der Infrastruktur überlassen, anstatt ihn über den Business-Code zu verteilen.
Fehler Nr. 5: „Es ist einfacher, CSRF für /login und /logout zu deaktivieren, dort gibt es ja keine Daten“.
Login/Logout sind Änderungen des security-state. Das Deaktivieren von CSRF genau an diesen Punkten bringt Sie zurück in die Welt von Forced Login und Forced Logout. Wenn Sie Ausnahmen machen, dann tun Sie es bewusst und mit Verständnis des Browser-Modells, nicht weil „es sich so einfacher testen lässt“.
GO TO FULL VERSION