Cross-Site Request Forgery (CSRF) is a critical web security vulnerability that allows attackers to trick users into performing actions they didn't intend to perform. This article explores CSRF protection mechanisms in Java web applications.
Understanding CSRF
CSRF attacks occur when a malicious website causes a user's browser to perform unwanted actions on a trusted site where the user is authenticated. For example, an attacker could force a user to change their email address, transfer funds, or make purchases without their consent.
CSRF Protection Strategies
1. Synchronizer Token Pattern
The most common CSRF protection method involves generating a unique, unpredictable token for each user session and including it in forms and AJAX requests.
2. SameSite Cookies
Using SameSite cookie attributes prevents browsers from sending cookies along with cross-site requests.
3. Custom Headers
Requiring custom headers for state-changing requests since they cannot be sent cross-origin without CORS permission.
Implementing CSRF Protection in Spring Security
Spring Security provides built-in CSRF protection that's enabled by default.
Configuration
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .ignoringRequestMatchers("/api/public/**") ) .authorizeHttpRequests(authz -> authz .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/login") .permitAll() ); return http.build(); } } Manual CSRF Token Management
@Controller public class CsrfController { @GetMapping("/form") public String showForm(Model model) { // CSRF token is automatically added by Thymeleaf return "form-page"; } @PostMapping("/update-profile") public String updateProfile(@Valid UserProfile profile) { // CSRF protection is automatic return "redirect:/success"; } } Custom CSRF Protection Implementation
For applications not using Spring Security, here's how to implement CSRF protection manually:
1. CSRF Token Generator
@Component public class CsrfTokenManager { private static final String CSRF_TOKEN_NAME = "X-CSRF-TOKEN"; private static final int TOKEN_LENGTH = 32; public String generateToken() { byte[] bytes = new byte[TOKEN_LENGTH]; new SecureRandom().nextBytes(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } public boolean isValidToken(String storedToken, String receivedToken) { return storedToken != null && storedToken.equals(receivedToken); } public String getTokenName() { return CSRF_TOKEN_NAME; } } 2. CSRF Filter
@WebFilter("/*") public class CsrfFilter implements Filter { @Inject private CsrfTokenManager tokenManager; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; HttpSession session = httpRequest.getSession(false); // Generate token for new sessions if (session != null && session.getAttribute(tokenManager.getTokenName()) == null) { String token = tokenManager.generateToken(); session.setAttribute(tokenManager.getTokenName(), token); } // Validate token for state-changing requests if (requiresCsrfProtection(httpRequest)) { String sessionToken = session != null ? (String) session.getAttribute(tokenManager.getTokenName()) : null; String requestToken = getTokenFromRequest(httpRequest); if (!tokenManager.isValidToken(sessionToken, requestToken)) { httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CSRF Token"); return; } } chain.doFilter(request, response); } private boolean requiresCsrfProtection(HttpServletRequest request) { String method = request.getMethod(); return "POST".equals(method) || "PUT".equals(method) || "DELETE".equals(method) || "PATCH".equals(method); } private String getTokenFromRequest(HttpServletRequest request) { // Check header first, then parameter String token = request.getHeader(tokenManager.getTokenName()); if (token == null) { token = request.getParameter(tokenManager.getTokenName()); } return token; } } 3. JSP with CSRF Protection
<%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>User Profile</title> </head> <body> <h2>Update Profile</h2> <form action="update-profile" method="post"> <input type="hidden" name="X-CSRF-TOKEN" value="${sessionScope['X-CSRF-TOKEN']}"> <label for="email">Email:</label> <input type="email" id="email" name="email" required> <label for="name">Name:</label> <input type="text" id="name" name="name" required> <button type="submit">Update</button> </form> <script> // For AJAX requests function getCsrfToken() { return '${sessionScope['X-CSRF-TOKEN']}'; } function updateProfile() { fetch('/update-profile', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, body: JSON.stringify({ email: document.getElementById('email').value, name: document.getElementById('name').value }) }); } </script> </body> </html> CSRF Protection for REST APIs
Stateless CSRF Protection
@RestController public class ApiController { @PostMapping("/api/transfer") public ResponseEntity<?> transferFunds(@RequestBody TransferRequest request, HttpServletRequest httpRequest) { // Validate CSRF token for state-changing operations String csrfToken = httpRequest.getHeader("X-CSRF-TOKEN"); if (!isValidCsrfToken(csrfToken)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } // Process transfer return ResponseEntity.ok().build(); } private boolean isValidCsrfToken(String token) { // Implement token validation logic return true; } } Advanced CSRF Protection Techniques
1. Double Submit Cookie Pattern
@Component public class DoubleSubmitCsrfProtection { public void addCsrfTokens(HttpServletResponse response) { String token = UUID.randomUUID().toString(); // Set cookie Cookie cookie = new Cookie("CSRF-TOKEN", token); cookie.setHttpOnly(false); cookie.setSecure(true); cookie.setPath("/"); response.addCookie(cookie); // Also set as header for JavaScript access response.setHeader("X-CSRF-TOKEN", token); } public boolean validateCsrfToken(HttpServletRequest request) { String cookieToken = getCookieValue(request, "CSRF-TOKEN"); String headerToken = request.getHeader("X-CSRF-TOKEN"); return cookieToken != null && cookieToken.equals(headerToken); } private String getCookieValue(HttpServletRequest request, String name) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (name.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } } 2. Encrypted Token Pattern
@Component public class EncryptedCsrfTokenManager { private final SecretKey secretKey; public EncryptedCsrfTokenManager() { // In production, load from secure configuration secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); } public String generateToken(String sessionId) { String tokenData = sessionId + "|" + System.currentTimeMillis(); return Jwts.builder() .setSubject(tokenData) .signWith(secretKey) .compact(); } public boolean validateToken(String token, String sessionId) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); String[] parts = claims.getSubject().split("\\|"); return parts.length == 2 && parts[0].equals(sessionId); } catch (JwtException e) { return false; } } } Testing CSRF Protection
Unit Tests
@SpringBootTest class CsrfProtectionTest { @Autowired private WebApplicationContext context; private MockMvc mockMvc; @BeforeEach void setup() { mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); } @Test void shouldRejectRequestWithoutCsrfToken() throws Exception { mockMvc.perform(post("/update-profile") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("email", "[email protected]")) .andExpect(status().isForbidden()); } @Test void shouldAcceptRequestWithValidCsrfToken() throws Exception { mockMvc.perform(post("/update-profile") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("email", "[email protected]") .with(csrf())) // Spring Security test support .andExpect(status().isOk()); } } Best Practices
- Enable CSRF Protection by Default: Always enable CSRF protection for state-changing operations
- Use Framework Support: Prefer framework-provided CSRF protection when available
- Secure Token Storage: Store tokens in HTTP-only cookies when possible
- Token Rotation: Consider rotating CSRF tokens after use for sensitive operations
- SameSite Cookies: Use
SameSite=StrictorSameSite=Laxfor session cookies - Exclude Public Endpoints: Disable CSRF protection for truly public APIs
- Log CSRF Failures: Monitor and log CSRF validation failures for security analysis
Common Pitfalls to Avoid
- Disabling CSRF protection without proper justification
- Storing CSRF tokens in localStorage (vulnerable to XSS)
- Using predictable CSRF tokens
- Forgetting to include CSRF tokens in AJAX requests
- Not validating CSRF tokens for all state-changing operations
Conclusion
CSRF protection is essential for web application security. Java frameworks like Spring Security provide robust, built-in CSRF protection that should be used whenever possible. For custom implementations, ensure you follow security best practices and thoroughly test your CSRF protection mechanisms.
Remember that CSRF protection is just one layer of defense in a comprehensive web application security strategy. Always combine it with other security measures like input validation, output encoding, and proper authentication/authorization.