Spring Security 403 Forbidden on POST → Fix CSRF (Spring Boot 3)
You add a simple form to your Spring Boot app. You hit “Submit”. You get a 403 Forbidden error.
You check the logs. Nothing. You check your permissions. They’re fine. You pull your hair out.
I’ve debugged this exact scenario for dozens of students at www.codegigs.app. It’s almost always Cross-Site Request Forgery (CSRF) protection doing its job a little too well. The default configuration blocks every state-changing request (POST, PUT, DELETE) unless you have a specific token.
Most tutorials tell you to just .csrf(c -> c.disable()). Don’t do that. Here’s how to actually fix it.
Why Is Spring Blocking My Requests?
Think of the CSRF token like a secret handshake.
When you log into your bank, your browser stores a session cookie. If you accidentally click a link on evil-site.com that tries to POST to your bank, your browser dutifully sends that session cookie along. The bank thinks it’s you.
Spring Security stops this by requiring a unique, random token for every POST request. evil-site.com doesn’t know the handshake, so Spring blocks it with a 403.
The problem? Your own frontend (React, Angular, or even Postman) doesn’t know the handshake either unless you tell it.
Scenario 1: The Fix for Thymeleaf (Server-Side)
If you’re building a classic Spring MVC app with Thymeleaf, this is usually painless. Spring injects the token automatically… if you use the right tags.
Here is the config that works out of the box in Spring Boot 3.2:
// SecurityConfig.java
// Spring Boot 3.2.1
package app.codegigs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(withDefaults());
// Note: We are NOT disabling CSRF here.
// It is on by default.
return http.build();
}
}
And here is the HTML form. If you write a plain HTML <form>, it will fail. You must use the Thymeleaf th:action attribute.
<!-- login.html -->
<!-- CORRECT: Thymeleaf automatically adds the hidden _csrf input -->
<form th:action="@{/login}" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<button type="submit">Login</button>
</form>
<!-- WRONG: This will return 403 Forbidden -->
<form action="/login" method="post">
...
</form>
When Thymeleaf renders that first form, it secretly inserts this:
<input type=”hidden” name=”_csrf” value=”a1b2c3d4…” />
That’s the handshake. If you still get a 403, check your browser’s “Inspect Element” to verify that hidden input exists. If it’s missing, your filter chain might be misconfigured.
Scenario 2: The Fix for React/Angular (SPAs)
This is where 90% of developers get stuck.
If you have a React frontend calling a Spring Boot backend, Thymeleaf can’t inject hidden inputs. You need a different strategy: Cookies.
You need to tell Spring to send the token in a cookie, and tell React to read that cookie. This is the pattern we teach in the full course at www.codegigs.app.
// SecurityConfig.java for SPA
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. Use the Cookie repository
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 2. This is required in Spring Security 6 to make the token visible
// to the frontend immediately
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
);
return http.build();
}
Why `withHttpOnlyFalse`?
By default, Spring marks the CSRF cookie as HttpOnly. This is great for security (JavaScript can’t read it), but it means your React app can’t read the token to send it back.
Setting it to false allows Axios/Fetch to read the XSRF-TOKEN cookie and include it in the X-XSRF-TOKEN header.
Frontend Setup (Axios):
// axios-config.js
import axios from 'axios';
// Most libraries do this automatically if the cookie exists,
// but setting it explicitly saves you headaches.
axios.defaults.withCredentials = true;
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
I saw a thread on Stack Overflow just last week where a developer spent 3 days debugging this. They had the Java config right, but forgot withCredentials = true in Axios. Without that, the browser refuses to send cookies.
When Should You DISABLE CSRF?
Ideally? Never.
Realistically? If you are using Stateless Authentication (like JWTs) where the token is stored in `localStorage` (not cookies), you generally don’t need CSRF protection.
Why? Because the browser doesn’t automatically send `localStorage` data. The attacker can’t force your browser to send the JWT because they can’t read it.
// Only do this if you are 100% stateless (JWT in headers)
http.csrf(csrf -> csrf.disable());
HttpOnly cookie (which is often safer for XSS), you MUST keep CSRF enabled. You are vulnerable to CSRF if you use cookies for auth. Period.
Troubleshooting Checklist
Still getting 403s? Run this 30-second audit:
- Check the Method: GET requests should work. If POST fails, it’s CSRF.
- Check the Logs: Enable Debug Logging. It will explicitly tell you “Invalid CSRF Token found”.
- Check the Headers: In Chrome DevTools, look at the failed request. Does it have an
X-XSRF-TOKENheader? If not, your frontend isn’t reading the cookie.
Secure Your Spring Apps properly
CSRF is just one piece of the puzzle. What about XSS? SQL Injection? OAuth2?
I’ve built a complete learning path covering every security aspect you’ll face in production.
Summary
Don’t be the developer who disables security features because they’re annoying.
- Use
th:actionfor Thymeleaf. - Use
CookieCsrfTokenRepository.withHttpOnlyFalse()for React/Angular. - Only disable CSRF if you are strictly using headers for auth (no cookies).
Next up, make sure your APIs are locked down properly by reading our guide on Securing Spring Boot APIs.