CodeGym /Kursy /Spring Security /CSRF: login, logout, and data

CSRF: login, logout, and data

Spring Security
Poziom 11, Lekcja 1
Dostępny

1. CSRF: data and security-state

If we step away from terminology for a second and look at the application through the lens of common sense, we will see that the system lives in states. A user has “logged in/logged out”, a draft has “DRAFT/SUBMITTED”, and a profile has “old value/new value”. CSRF does not care about “favorite” entities; it cares about one thing: the request changes the state of something important.

At this point, it helps to draw a very clear line between two kinds of “state” that requests can change. Business state is your domain data: profile, drafts, content statuses. Security-state is who the browser is considered to be in the current session: anonymous or a specific user, and what role set they have. And login/logout are exactly the switches for security-state.

Let us fix this in a small table — no philosophy, as practical as possible:

Operation What actually changes Why this is a CSRF topic
PATCH /api/me/profile User profile (business state) The browser may send the request with a cookie on its own; you need to confirm that the action was initiated by your UI
DELETE /api/drafts/{id} Data (the draft disappears) The same thing: “deletion” is a strong state change
POST /login (in formLogin) Security-state: the session becomes authenticated If an attacker “plants” a login, the victim ends up logged in to the wrong account
POST /logout Security-state: the session stops being authenticated If logout can be forged, the user can be “kicked out” of the system without intending it

So CSRF protection is not about “data vs. not data.” It is about “changes state vs. does not change state.” Remember the wording from the first lecture: the vulnerability lives where the browser automatically attaches authentication context. And login/logout are exactly what change which context will be attached next.

2. Login CSRF and changing security-state

When we say “login”, the brain automatically draws a picture: the user enters a login/password, clicks “Log in”, the system checks it — and that is it. But from an attacker’s point of view, a different question is more interesting: can we force the victim’s browser to perform a login without their intention? That is the typical “login CSRF” scenario (sometimes called “forced login”).

The danger is that login changes more than just some flag. It changes the answer to the question “who are you?” within the current session. After that, all your protected endpoints start working “on behalf of” whoever the session became. And if the attacker managed to plant a login to their account, the victim may start acting in the system as “someone else” without noticing the swap, especially if the UI does not show the username very clearly.

Let us imagine a nasty but realistic story. A user opens some third-party site (say, “10 ways to speed up Java by 200 times”, we have all been there). That site sneaks a request into the browser for POST /login, with the attacker account login/password in the form body. If your application login endpoint is not protected by CSRF mechanics, the server can authenticate the session and “bind” it to the attacker’s account. Then the user honestly goes to your Secure Content Platform API, creates a draft, writes something personal there... only they are writing it into the attacker’s account. Congratulations: “data theft” happened without stealing the victim’s password.

Let us draw this scenario as a diagram. It is a bit theatrical, but useful — it helps you see the mechanics visually:

sequenceDiagram
    %% Idea: the attacker initiates a login to their account through the victim's browser
    %% Key point: the browser automatically attaches cookies (if any)

    participant V as "Victim (browser)"
    participant E as "Evil site"
    participant A as "Secure Content Platform API"

    V->>E: Opens the attacker's page
    E->>V: "Sneaks in an auto-submit form to /login"
    V->>A: "POST /login (attacker login/password) + cookie, if present"
    A->>A: "Authentication, filling SecurityContext, session becomes authenticated"
    A-->>V: "302/200 (depends on flow)"
    V->>A: "Then the victim visits /api/me, /api/drafts..."
    A->>A: "The server thinks this is the attacker's account"

And here is the main takeaway of the lecture: login is a state-changing operation because it changes the security identity of the current session. Therefore, you need to protect login with a CSRF token, not only “business” PATCH/DELETE, but also login itself.

The link to Spring Security and formLogin

The good news is that if you use standard formLogin and do not break the settings outright, Spring Security already treats login as a state-changing operation by default. For example, the default login page that Spring shows out of the box sends POST and contains a CSRF token as a hidden form field. What matters now is the fact itself: even login requires a CSRF token, because it changes security state.

In our application, SecurityFilterChain already contains formLogin and logout. In minimal form, it looks like this (a fragment you insert into the existing config):

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 {
    // Enable standard formLogin: handling POST /login will be part of the security layer
    http.formLogin(form -> {});

    // Logout also changes security-state (session invalidation / clearing the security context)
    http.logout(logout -> {});

    // Build the Spring Security filter chain
    return http.build();
}

This is a simple but important idea: login/logout live not in “business controllers” but in the security layer, because they change exactly the security-state. So CSRF logic lives there too.

3. Logout: you cannot log out with GET

Logout is an even better example, because beginners often see it as an “innocent button”: well, the user logged out, nothing was deleted. In reality, logout is an operation that changes the system’s security state: you kick the user out of the authenticated state and clear/invalidate what made them “logged in” (most often the session or the SecurityContext in the session).

If logout can be forged, an attacker gets the ability to “flip the switch” on someone else’s session. This may not be as dramatic as deleting data, but it is still very real: constant forced logouts can break user workflows, prevent them from editing a profile, creating drafts, or performing important actions. And sometimes (in complex systems), forced logout can be part of a larger attack: kicked out of the system → prompted into the “wrong” account again → the victim keeps working in the wrong context.

Now the key point that is worth remembering as a rule of thumb: if CSRF is enabled, Spring Security by default makes logout use POST, not GET. Why? Because GET is supposed to be a “safe method” (in an ideal architecture — pure read-only), while logout is a state change. Making logout a GET is like hanging a “Log out” button on a billboard: anyone can “click” it for you by making the browser load a URL.

If you imagine a bad world where logout is available via GET /logout, then an attacker does not even need JavaScript. It is enough to put an image on the page:

<!-- BAD: if /logout is available via GET, the browser will hit it immediately when loading the image -->
<img src="https://your-site/logout">

The browser will “politely” load that image, automatically send your session cookie — and that is it, the user is logged out. The image, of course, will not load (because the URL is not an image), but logout has already happened, and the browser is not offended. It rarely is, really; it just works.

Spring Security tries to protect you from this “innocent” self-deception. So in a normal configuration, even if you did not customise anything, logout is POST /logout and protected by CSRF.

Anti-pattern: “Let us allow logout via GET; it is more convenient”

Sometimes a developer sees that logout does not work as a “link” (or in the browser after a direct visit to /logout) and chooses a “make it work” solution: allow logout via GET. Technically, you can do that, and Spring allows it, but in the context of CSRF this is a good example of what not to do.

Here is what such a “shortcut” looks like (shown as an anti-example — not a recommendation):

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

// BAD: logout via GET turns logout into a CSRF target
http.logout(logout -> logout
    // Here we explicitly tell Spring Security: "treat GET /logout as a logout request"
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
);

This line makes logout a “link.” But at the same time, it makes logout an easy target for request forgery. In the training project, we are exactly learning to see such solutions not as “yay, it works”, but as “what have I just broken in the security model?”

4. CSRF vs roles: different checks

A very common confusion among beginners looks like this: “If my endpoint requires authenticated(), why do I also need CSRF? The attacker is not logged in anyway!” And this is where you need to remember the key idea: CSRF works exactly in the scenario where the user is already logged in, and the browser automatically attaches the session cookie.

Role/authority checks answer the question “is this user allowed?” CSRF answers the question “was this action initiated by a correct scenario?” These are two independent checks, and both can be true or false independently.

Imagine the request PATCH /api/me/profile. It should be allowed for a user who is logged in (authorization), but it should also be initiated by “our UI” (CSRF). If CSRF is disabled, the attacker can initiate the request on behalf of the logged-in user, and authorization will honestly say “yes, this is USER, they are allowed to change the profile.” Spring Security is not telepathic: if you did not enable CSRF, it will not know that the request came from another page.

That is why in SecurityFilterChain you usually see both access rules and CSRF protection at the same time. Roughly in this style (highly simplified, just to see the idea):

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: "who is allowed" (permission/authentication checks)
        .requestMatchers(HttpMethod.PATCH, "/api/me/profile").authenticated()
        .anyRequest().permitAll()
    );

    // Important: CSRF is a separate check, it lives in filters and is enabled by separate configuration
    return http.build();
}

Yes, we do not write “csrf” explicitly here — yet. But the idea is this: authorization defines “who is allowed,” CSRF defines “what a correct browser-originated request should look like.” And login/logout are exactly the same state-changing operations, only they change not the profile, but the security-state itself.

5. Where this shows up in the project

In the project, it helps to keep one rule in mind: in the session-based branch, CSRF applies to all unsafe methods that change either business state or security-state. Profiles, drafts, and submit scenarios follow the same logic as any other PATCH / POST / DELETE: the cookie shows that the browser has a session, but it does not prove that the action was actually initiated by your interface.

For this lecture, two points that are easy to underestimate are especially important:

— POST /login — changes security-state: the session becomes authenticated.

— POST /logout — also changes security-state: the session is cleared, and the browser stops being considered a logged-in user.

This helps you avoid getting stuck in the false picture that “CSRF is only about PATCHing profiles and DELETEing drafts.” It appears wherever the browser changes state on a cookie-based session — only in some places that state is domain data, and in others it is the security picture of the user itself.

6. Typical mistakes with CSRF login/logout

Mistake #1: “CSRF is only about business data, and login/logout have nothing to do with it.”
This way of thinking appears when we treat the system’s state as only the tables in the database, while seeing security-state as “just some technical nonsense.” In practice, security-state is just as much a system state as a draft status. Login and logout change that state, and an attacker often benefits from toggling exactly these switches to control user behaviour.

Mistake #2: “Logout is harmless; let it be GET, it is more convenient.”
Convenience in the browser is often deceptive. Logout via GET turns into a link-like operation that is easy to initiate from another page. Yes, it does not delete all data, but it is a real lever: the user can be kicked out of the system, breaking workflows. In the training project, we practice choosing security and predictability, not “whatever is easiest to click.”

Mistake #3: “If the endpoint is protected by roles, CSRF is not needed.”
Roles and CSRF solve different problems. Roles say “this user has permission,” CSRF says “this action came from a correct UI scenario.” In a CSRF attack, the user already has a role and is already logged in — otherwise the attack usually just does not work. So the role does not save you from CSRF; it simply exists in parallel.

Mistake #4: “If I do not see CSRF in the controller code, then it does not exist.”
This is classic Spring Security: the CSRF check is performed at the filter level, before the controller. So the absence of a “manual token check” in the controller method is actually a normal sign: you entrusted protection to the infrastructure instead of spreading it through business code.

Mistake #5: “It is easier to disable CSRF on /login and /logout; there is no data there anyway.”
Login/logout are a change in security-state, which means disabling CSRF specifically at these points takes you back to the world of forced login and forced logout. If you make exceptions, make them deliberately and with an understanding of the browser model, not because “it is easier to test that way.”

1
Zadanie
Spring Security, poziom 11, lekcja 1
Niedostępne
Default login page jako część security-state
Default login page jako część security-state
1
Zadanie
Spring Security, poziom 11, lekcja 1
Niedostępne
Audyt Bash – akcje read‑only, business‑state i security‑state
Audyt Bash – akcje read‑only, business‑state i security‑state
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION