CSRF disabled, CORS wildcard, permitAll() on everything — these Spring Security mistakes are common, hard to spot, and exploitable. Learn how to detect and fix them.
JOptimize Team
Spring Security is powerful, but misconfiguring it is dangerously easy. A single wrong line in your SecurityConfig can expose your entire application to attacks — and the worst part is that everything still compiles and runs fine.
In this article, we cover the most common Spring Security misconfigurations, why they're dangerous, and how to fix them properly.
This is the single most common mistake in Spring Boot applications:
http.csrf().disable(); // ❌ Never do this in a stateful app
Why it's dangerous: CSRF (Cross-Site Request Forgery) attacks trick authenticated users into executing actions they didn't intend to. When you disable CSRF, any malicious website can send requests to your API on behalf of a logged-in user — transferring money, changing passwords, deleting accounts.
When it's actually OK: Stateless REST APIs that use token-based authentication (JWT, API keys) don't need CSRF protection because they don't rely on cookies. But you need to be intentional about it.
The fix:
// For stateless JWT APIs — explicitly document the reason http.csrf(csrf -> csrf.disable()); // OK — stateless, JWT auth, no session cookies // For stateful apps — keep CSRF enabled (it's the default) // Or configure it properly: http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) );
config.addAllowedOrigin("*"); // ❌ Allows any website to call your API
Why it's dangerous: With a wildcard CORS policy, any JavaScript running on any domain can make authenticated requests to your API. Combined with credentials, this becomes a serious attack vector.
The fix:
@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( "https://yourdomain.com", "https://www.yourdomain.com", "http://localhost:3000" // dev only )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; }
Never use allowedOrigins("*") with allowCredentials(true) — Spring will throw an exception for this combination. But using "*" without credentials is still a bad idea for authenticated APIs.
http.authorizeHttpRequests(auth -> auth .anyRequest().permitAll() // ❌ Every endpoint is public );
Why it's dangerous: This silently makes all your endpoints — including admin routes, user data, payment endpoints — publicly accessible. No authentication required. It's often added as a "quick fix" during development and forgotten in production.
The fix:
http.authorizeHttpRequests(auth -> auth // Public endpoints .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/webhooks/**").permitAll() .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // Admin only .requestMatchers("/api/admin/**").hasRole("ADMIN") // Everything else requires authentication .anyRequest().authenticated() );
Be explicit. Default to .anyRequest().authenticated() and whitelist only what needs to be public.
Even with .anyRequest().authenticated(), you can still expose write operations to users who shouldn't have access:
@RestController @RequestMapping("/api/users") public class UserController { @PostMapping // ❌ Any authenticated user can create users public User createUser(@RequestBody User user) { ... } @DeleteMapping("/{id}") // ❌ Any user can delete any account public void deleteUser(@PathVariable Long id) { ... } @PutMapping("/admin/reset-password") // ❌ Should be admin only public void resetPassword(...) { ... } }
The fix:
@RestController @RequestMapping("/api/users") public class UserController { @PostMapping @PreAuthorize("hasRole('ADMIN')") public User createUser(@RequestBody User user) { ... } @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id") public void deleteUser(@PathVariable Long id) { ... } @PutMapping("/admin/reset-password") @PreAuthorize("hasRole('ADMIN')") public void resetPassword(...) { ... } }
Enable method security in your config:
@Configuration @EnableMethodSecurity // Required for @PreAuthorize to work public class SecurityConfig { ... }
By default, Spring Security adds some security headers, but not all. Missing headers leave your app vulnerable to clickjacking, XSS, and MIME sniffing attacks.
// Check what you're actually sending: // X-Frame-Options: DENY ✓ (Spring adds this) // X-Content-Type-Options: nosniff ✓ (Spring adds this) // Strict-Transport-Security: ✗ (not added by default in dev) // Content-Security-Policy: ✗ (not added by default)
The fix:
http.headers(headers -> headers .frameOptions(frame -> frame.deny()) .contentTypeOptions(Customizer.withDefaults()) .httpStrictTransportSecurity(hsts -> hsts .maxAgeInSeconds(31536000) .includeSubDomains(true) ) .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'") ) );
// ❌ Default behavior — stack traces in production responses { "timestamp": "2026-04-15T10:00:00.000+00:00", "status": 500, "error": "Internal Server Error", "trace": "java.lang.NullPointerException\n\tat com.example.UserService.getUser(UserService.java:45)\n\t...", "path": "/api/users/123" }
Why it's dangerous: Stack traces reveal your internal package structure, class names, library versions, and exact line numbers — giving attackers a detailed map of your application.
The fix:
// application.properties server.error.include-stacktrace=never server.error.include-message=never server.error.include-binding-errors=never
And add a global exception handler:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public ResponseEntity<Map<String, String>> handleException(Exception e) { log.error("Unhandled exception", e); // Log internally return ResponseEntity.status(500) .body(Map.of("error", "An unexpected error occurred")); // Generic message to client } @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<Map<String, String>> handleNotFound(ResourceNotFoundException e) { return ResponseEntity.status(404) .body(Map.of("error", "Resource not found")); } }
JOptimize automatically scans your Spring Security configuration and flags these misconfigurations:
joptimize analyze . # → [CRITICAL] CSRF disabled — stateful app without CSRF protection # SecurityConfig.java:24 # # → [CRITICAL] CORS wildcard detected — all origins permitted # SecurityConfig.java:51 # # → [CRITICAL] anyRequest().permitAll() — all endpoints are public # SecurityConfig.java:35 # # → [WARNING] Write endpoint 'createUser()' has no access control annotation # UserController.java:18
Each issue comes with an explanation and a concrete fix — no manual security audit required.
"*" in production.anyRequest().authenticated(), whitelist public routes explicitly@PreAuthorize on sensitive endpointsIf you want to go deeper on Spring Security, this course covers everything from the basics to OAuth2, JWT, and production-grade configurations:
Spring Security — Zero to Master — one of the most complete Spring Security courses on Udemy, covering authentication, authorization, OAuth2, JWT, and more with hands-on projects.
Run JOptimize to find all security misconfigurations in your Spring Boot project:
npm install -g @joptimize/cli joptimize auth jp_live_your_key joptimize analyze .
Or upload your ZIP at joptimize.io.
Master Spring Boot, security, and Java performance with hands-on courses.
JOptimize finds N+1 queries, EAGER collections, and 70+ other issues in your Java codebase — in under 30 seconds.