CSRF Protection in Web Applications with Java

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

  1. Enable CSRF Protection by Default: Always enable CSRF protection for state-changing operations
  2. Use Framework Support: Prefer framework-provided CSRF protection when available
  3. Secure Token Storage: Store tokens in HTTP-only cookies when possible
  4. Token Rotation: Consider rotating CSRF tokens after use for sensitive operations
  5. SameSite Cookies: Use SameSite=Strict or SameSite=Lax for session cookies
  6. Exclude Public Endpoints: Disable CSRF protection for truly public APIs
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper