RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
MediumSpring Boot#537 min readJul 11, 2026

CSRF protection.

practice
learning
Practice modeTest yourself instead of reading straight through

Hook: Interviewers love this question because CSRF is the classic 'my browser did it for me' bug.

Question: What is CSRF protection in Spring Boot?

Answer: CSRF stands for Cross-Site Request Forgery. It protects you from a malicious site tricking a logged-in user's browser into sending an unwanted state-changing request, like a POST or DELETE, to your app. Spring Security usually blocks those requests unless the browser sends the right CSRF token.

Interview-Ready Answer: In Spring Boot, CSRF protection is Spring Security's way of checking that a state-changing request really came from my application, not from a forged form on another site. It works by creating a random token, storing it in the session or a cookie, and requiring the client to send that token back in a form field or a request header. By default it is enabled for browser-based apps, and for SPAs I usually send the token in a header instead of a hidden field.

🧠 Memory Map
Memory map — visual summary of this topic

What CSRF is, in simple words

CSRF is different from hacking the password. The attacker does not need to know the user's password. The trick is that a browser automatically sends cookies, so if the user is already logged in, a hidden form on another site can ride along with that session cookie. A good mental model is: a session cookie is like a stamped wristband; if the browser brings the wristband, your server may assume the request is legit unless you also require a second proof.

How Spring Security checks it under the hood

  1. A user logs in, usually with a session cookie.
  2. Spring Security generates a CSRF token, which is a random value linked to that session or stored in a cookie.
  3. Your page or client reads that token and sends it back on unsafe requests such as POST, PUT, PATCH, or DELETE.
  4. Spring's CsrfFilter runs before the controller. A filter is a request interceptor that can accept, modify, or reject the request.
  5. The filter compares the submitted token with the stored token.
  6. If they match, the request continues to your controller. If they do not match, Spring returns 403 Forbidden and the controller never runs.

By default, Spring Security protects the unsafe HTTP methods and ignores safe ones like GET, HEAD, OPTIONS, and TRACE. That is important because GET should not change server state.

When to use it

Use CSRF protection whenever the browser automatically sends credentials, especially session cookies or remember-me cookies. That is the normal case for server-rendered Spring MVC apps. You usually keep it on for login forms too, because login is also a state-changing operation.

For stateless APIs that use an Authorization: Bearer ... header and do not rely on cookies, CSRF is often unnecessary. The browser does not automatically invent that header the way it invents cookies. But if you store JWTs in cookies, CSRF risk comes back.

Session token vs cookie token

OptionBest forWhere token livesMain trade-off
HttpSessionCsrfTokenRepositoryServer-rendered formsServer sessionSimple, but needs a session
CookieCsrfTokenRepositorySPAs / AJAXCookie plus headerEasy for JavaScript, but JS can read the cookie

In practice, the session-based option is the easiest for Thymeleaf or JSP pages. The cookie-based option is common for single-page apps because JavaScript can copy the token from the cookie into a header such as X-CSRF-TOKEN.

Performance and edge cases

CSRF validation is cheap: roughly O(1) time per protected request, because Spring just looks up one token and compares it. Memory overhead is tiny too, usually one small random token per session, plus the normal session cost. The real cost is not CPU; it is wiring the frontend correctly.

Common edge cases include stale cached forms, missing tokens after login, and endpoints that are intentionally public. Also remember that SameSite cookies help but do not replace CSRF protection. SameSite is a browser cookie rule that limits when cookies travel cross-site, but it is not a full server-side defense and it can behave differently across browsers and flows.

Compared with disabling CSRF, the safer rule is: if the browser auto-sends credentials, require a token; if the client explicitly adds credentials in a header, CSRF risk is much lower. Another useful split is CSRF versus CORS: CORS controls which origins may read responses, while CSRF controls whether a forged request is accepted. They solve different problems.

Real-world story

Imagine a checkout service in an e-commerce app. A signed-in customer can update their shipping address from /checkout/address. If CSRF is disabled, an attacker can place a hidden form on a malicious site that auto-submits to your app as soon as the victim visits. The victim never sees the request, but the browser still sends the session cookie, so the address change succeeds.

The symptoms are sneaky: support tickets say packages were rerouted, audit logs show a normal user session, and the server logs contain unexpected POSTs to address or payment endpoints. If CSRF is enabled but your frontend forgot the token, the symptom is different: users suddenly get 403 Forbidden on form submit, and you may see logs such as invalid or missing CSRF token. In both cases, the browser is involved; the difference is whether the forged request is blocked or allowed.

This is why CSRF is most important on actions that move money, change email or address, or mutate account settings. Read-only pages are not the target; dangerous state changes are.

Spring Boot
// src/main/java/com/example/csrfdemo/CsrfProtectionApplication.java
package com.example.csrfdemo;

import java.security.Principal;
import java.util.concurrent.atomic.AtomicReference;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.HtmlUtils;

@SpringBootApplication
public class CsrfProtectionApplication {
    public static void main(String[] args) {
        SpringApplication.run(CsrfProtectionApplication.class, args);
    }
}

@Configuration
class SecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Keep CSRF ON: Spring Security will reject unsafe requests without a valid token.
            .csrf(Customizer.withDefaults())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/error").permitAll()
                .anyRequest().authenticated())
            .formLogin(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    UserDetailsService users() {
        return new InMemoryUserDetailsManager(
            User.withUsername("alice")
                .password("{noop}password")
                .roles("USER")
                .build()
        );
    }
}

@RestController
class ProfileController {
    private final AtomicReference<String> nickname = new AtomicReference<>("Alice");

    @GetMapping(value = "/profile", produces = MediaType.TEXT_HTML_VALUE)
    ResponseEntity<String> profilePage(Principal principal, CsrfToken csrfToken) {
        // The token is not a password; it is proof that this page came from our app.
        String html = """
            <!doctype html>
            <html>
              <body>
                <h1>Profile for %s</h1>
                <p>Current nickname: <b>%s</b></p>
                <form method='post' action='/profile'>
                  <input type='hidden' name='%s' value='%s'/>
                  <label>New nickname: <input name='nickname'/></label>
                  <button type='submit'>Save</button>
                </form>
              </body>
            </html>
            """.formatted(
                HtmlUtils.htmlEscape(principal.getName()),
                HtmlUtils.htmlEscape(nickname.get()),
                csrfToken.getParameterName(),
                csrfToken.getToken()
            );
        return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(html);
    }

    @PostMapping(value = "/profile", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_HTML_VALUE)
    ResponseEntity<String> updateNickname(@RequestParam(required = false) String nickname) {
        // Edge case: CSRF does not validate user input. Blank input is a separate problem.
        if (nickname == null || nickname.isBlank()) {
            return ResponseEntity.badRequest()
                .contentType(MediaType.TEXT_HTML)
                .body("<p>Nickname cannot be blank.</p>");
        }

        this.nickname.set(nickname.trim());
        return ResponseEntity.ok()
            .contentType(MediaType.TEXT_HTML)
            .body("<p>Saved nickname to: " + HtmlUtils.htmlEscape(this.nickname.get()) + "</p>");
    }
}

// src/test/java/com/example/csrfdemo/CsrfProtectionApplicationTests.java
package com.example.csrfdemo;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class CsrfProtectionApplicationTests {

    @Autowired
    MockMvc mvc;

    @Test
    void postWithoutCsrfTokenIsRejected() throws Exception {
        // This is the failure path interviewers care about: the controller is never reached.
        mvc.perform(post("/profile").with(user("alice").roles("USER")).param("nickname", "Neo"))
            .andExpect(status().isForbidden());
    }

    @Test
    void postWithCsrfTokenIsAccepted() throws Exception {
        mvc.perform(post("/profile").with(user("alice").roles("USER")).with(csrf()).param("nickname", "Neo"))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("Saved nickname to: Neo")));
    }

    @Test
    void blankNicknameStillFailsValidationEvenWithCsrf() throws Exception {
        mvc.perform(post("/profile").with(user("alice").roles("USER")).with(csrf()).param("nickname", "   "))
            .andExpect(status().isBadRequest())
            .andExpect(content().string(containsString("Nickname cannot be blank")));
    }

    @Test
    void getProfilePageIncludesAHiddenCsrfField() throws Exception {
        mvc.perform(get("/profile").with(user("alice").roles("USER")))
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("name='_csrf'")));
    }
}

Follow-up & Tricky Questions:

  • How does Spring Security actually validate CSRF? It compares the token sent by the client with the token stored for that session or cookie. If they differ or the token is missing, the request is rejected before your controller runs.
  • How do I send CSRF tokens from a Thymeleaf form? Spring can expose the token as a request attribute, and Thymeleaf can render it into a hidden input. That hidden field is the standard browser-form approach.
  • How do SPAs handle CSRF? A common pattern is CookieCsrfTokenRepository, then JavaScript reads the token cookie and echoes it in a header like X-CSRF-TOKEN. The server checks that header instead of a form field.
  • When would I disable CSRF in Spring Security? Usually only for truly stateless endpoints that do not rely on cookies, such as a pure bearer-token API. Even then, disable it narrowly for those routes rather than turning it off for the whole app.
  • What is the difference between CSRF and CORS? CSRF stops forged requests from being accepted; CORS stops other origins from reading your responses in the browser. They overlap in the same app, but they are not the same control.
  • Does SameSite=Strict remove the need for CSRF? No. SameSite helps, but it is a browser feature with limits and edge cases, so you should not treat it as a full server-side replacement.
  • If I use JWT, can I always ignore CSRF? Not always. JWT in an Authorization header is usually safe from CSRF, but JWT stored in cookies is still vulnerable because cookies are auto-sent by the browser.
  • Are GET requests protected by CSRF? Normally no, because GET should be read-only. If a GET endpoint changes data, that is a design bug as well as a security bug.

Common Mistakes:

  • Turning CSRF off for the whole app. Correction: disable it only for endpoints that are truly stateless and protected by another mechanism.
  • Assuming CORS solves it. Correction: CORS controls who can read responses in the browser; CSRF controls whether your server accepts a forged request.
  • Forgetting the token in forms or AJAX calls. Correction: render a hidden field for server pages, or send the token in a header for SPAs.
  • Thinking SameSite cookies make CSRF impossible. Correction: SameSite is helpful but not a complete replacement for server-side validation.

Memory Hook: Think stamp plus envelope: the session cookie is the stamp that lets the browser enter, and the CSRF token is the sealed envelope that proves the request came from your own form.

Cheat Sheet:

  • CSRF = forged request sent from another site using the user's browser.
  • Spring Security protects unsafe methods by default.
  • Token must come back in a hidden field or request header.
  • Session repo is great for MVC; cookie repo is common for SPAs.
  • Missing or wrong token = 403 before controller execution.
  • Stateless bearer-token APIs often do not need CSRF, but cookie-based auth does.

Practice Tasks:

  • Add a new POST endpoint such as /address and verify that csrf() is required in a MockMvc test.
  • Switch the demo to CookieCsrfTokenRepository.withHttpOnlyFalse() and send the token in a custom header.
  • Deliberately remove the hidden token from the HTML form and confirm that the browser gets 403 Forbidden.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

// src/main/java/com/example/csrfdemo/CsrfProtectionApplication.java package com.example.csrfdemo; import java.security.Principal; import java.util.concurrent.atomic.AtomicReference; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.HtmlUtils; @SpringBootApplication public class CsrfProtectionApplication { public static void main(String[] args) { SpringApplication.run(CsrfProtectionApplication.class, args); } } @Configuration class SecurityConfig { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // Keep CSRF ON: Spring Security will reject unsafe requests without a valid token. .csrf(Customizer.withDefaults()) .authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/error").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); } @Bean UserDetailsService users() { return new InMemoryUserDetailsManager( User.withUsername("alice") .password("{noop}password") .roles("USER") .build() ); } } @RestController class ProfileController { private final AtomicReference<String> nickname = new AtomicReference<>("Alice"); @GetMapping(value = "/profile", produces = MediaType.TEXT_HTML_VALUE) ResponseEntity<String> profilePage(Principal principal, CsrfToken csrfToken) { // The token is not a password; it is proof that this page came from our app. String html = """ <!doctype html> <html> <body> <h1>Profile for %s</h1> <p>Current nickname: <b>%s</b></p> <form method='post' action='/profile'> <input type='hidden' name='%s' value='%s'/> <label>New nickname: <input name='nickname'/></label> <button type='submit'>Save</button> </form> </body> </html> """.formatted( HtmlUtils.htmlEscape(principal.getName()), HtmlUtils.htmlEscape(nickname.get()), csrfToken.getParameterName(), csrfToken.getToken() ); return ResponseEntity.ok().contentType(MediaType.TEXT_HTML).body(html); } @PostMapping(value = "/profile", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_HTML_VALUE) ResponseEntity<String> updateNickname(@RequestParam(required = false) String nickname) { // Edge case: CSRF does not validate user input. Blank input is a separate problem. if (nickname == null || nickname.isBlank()) { return ResponseEntity.badRequest() .contentType(MediaType.TEXT_HTML) .body("<p>Nickname cannot be blank.</p>"); } this.nickname.set(nickname.trim()); return ResponseEntity.ok() .contentType(MediaType.TEXT_HTML) .body("<p>Saved nickname to: " + HtmlUtils.htmlEscape(this.nickname.get()) + "</p>"); } } // src/test/java/com/example/csrfdemo/CsrfProtectionApplicationTests.java package com.example.csrfdemo; import static org.hamcrest.Matchers.containsString; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; @SpringBootTest @AutoConfigureMockMvc class CsrfProtectionApplicationTests { @Autowired MockMvc mvc; @Test void postWithoutCsrfTokenIsRejected() throws Exception { // This is the failure path interviewers care about: the controller is never reached. mvc.perform(post("/profile").with(user("alice").roles("USER")).param("nickname", "Neo")) .andExpect(status().isForbidden()); } @Test void postWithCsrfTokenIsAccepted() throws Exception { mvc.perform(post("/profile").with(user("alice").roles("USER")).with(csrf()).param("nickname", "Neo")) .andExpect(status().isOk()) .andExpect(content().string(containsString("Saved nickname to: Neo"))); } @Test void blankNicknameStillFailsValidationEvenWithCsrf() throws Exception { mvc.perform(post("/profile").with(user("alice").roles("USER")).with(csrf()).param("nickname", " ")) .andExpect(status().isBadRequest()) .andExpect(content().string(containsString("Nickname cannot be blank"))); } @Test void getProfilePageIncludesAHiddenCsrfField() throws Exception { mvc.perform(get("/profile").with(user("alice").roles("USER"))) .andExpect(status().isOk()) .andExpect(content().string(containsString("name='_csrf'"))); } }