Spring Boot Security
When to use
This skill is the Spring-specific layer on top of secure-coding and api-security. Spring Security is powerful and exactly therefore foot-gun-rich: small letters in the config decide whether your app is safe or wide open.
Triggers on:
- A question like "review our Spring Security config", "OAuth2 client setup", "lock down actuator endpoints", "JWT validation on a Spring Resource Server", "@PreAuthorize review".
- Presence of
spring-boot-starter-security, spring-security-oauth2-client, SecurityFilterChain beans, @EnableWebSecurity, @PreAuthorize/@PostAuthorize annotations, application.yml with spring.security.* or management.*.
- A PR that touches
.permitAll(), .disable() on CSRF/CORS, custom AuthenticationProvider, UserDetailsService, or a JWT filter.
- Spring version bumps around security releases.
- A handoff from
security-review or api-security where Spring is in the stack.
When NOT (handoff)
- General Java secure-coding →
secure-coding.
- API design and OWASP API Top 10 — large overlap, but that skill is framework-agnostic. Use
api-security for conceptual questions (what is IDOR, how do you validate a schema), this skill for Spring-specific implementation.
- SAST with Semgrep
p/java-spring or CodeQL → sast-orchestrator.
- Dep vulns (incl. Log4Shell-class vulns in transitive deps) →
cve-triage.
- Infra around Spring (Kubernetes deploy, container image) →
container-hardening + k8s-security.
- Secrets (application.properties with DB passwords) →
secrets-scanner.
Approach
Six phases. Phase 1 (SecurityFilterChain) is where most production bugs live.
1. SecurityFilterChain config
Spring Security 6+ uses the Lambda DSL. Each chain decides which auth mode goes with which path and what "open" means.
Common foot-guns:
.permitAll() on too broad a path. http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/**").permitAll()) opens the entire API. Look for .permitAll() on wildcards and challenge each one.
- Order of matchers. The filter chain matches top-down. A
/api/** permitAll above a /api/admin/** authenticated rule overrules the latter. Always go from specific to general.
.anyRequest().permitAll() — as the last rule this is catch-all open. Almost always wrong. The last rule should be .anyRequest().authenticated(), with an exception for a purely public app.
- Multiple
SecurityFilterChain beans with @Order. First match wins. A too-broad first chain can render later chains redundant.
.csrf(csrf -> csrf.disable()) without context. CSRF on by default for stateful web auth. Disabling is correct for stateless JWT APIs, wrong for form-based auth. If in doubt: leave it on.
- CORS config.
.cors(cors -> cors.configurationSource(source)) with a CorsConfigurationSource that returns wildcards is a standard misconception. allowedOrigins("*") together with allowCredentials(true) does not work (Spring rejects it), but it is a signal that the config flow has not been thought through.
Concrete reference config for a stateless JWT API:
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()))
.csrf(csrf -> csrf.disable()) // correct for stateless API
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
2. Method-level authorization
Route-level auth is never enough. Methods with sensitive logic deserve @PreAuthorize or equivalent.
@EnableMethodSecurity (Spring Security 6) replaces @EnableGlobalMethodSecurity. Without this annotation, @PreAuthorize/@PostAuthorize do not work.
@PreAuthorize("hasRole('ADMIN')") — role-based.
@PreAuthorize("hasAuthority('SCOPE_write:documents')") — for JWT scopes.
@PreAuthorize("#id == authentication.name or hasRole('ADMIN')") — per-resource ownership, the fix for IDOR (see api-security API1 BOLA).
@PostAuthorize — filter the return value after execution. Rarely needed, has performance impact.
Common miss: ownership check in the service method but not in the controller, or vice versa. One place is enough, but it must be unambiguous which one.
3. Actuator lockdown
Spring Boot Actuator exposes operational endpoints. The default config (older) was wide open. Since Boot 2.x the defaults are /health and /info public, the rest authenticated. Reviewer rule: confirm you have not fallen back to the older model.
management.endpoints.web.exposure.include — what gets exposed. "*" is wrong in prod. Limit to what Ops actually needs: health,info,prometheus,metrics.
management.endpoint.env.show-values=NEVER or ALWAYS/WHEN_AUTHORIZED. /env shows all env vars, including secrets when misconfigured.
management.endpoint.heapdump.enabled=false in prod. Heap dump via HTTP is a memory-exfil primitive.
management.server.port on a separate port that is not externally routable. Plus firewall/NetworkPolicy (see k8s-security).
- Actuator security on the SecurityFilterChain: a separate chain with
EndpointRequest.toAnyEndpoint() matcher, role ACTUATOR or equivalent.
4. JWT and OAuth2
Spring has three OAuth2 roles: client (consumer), resource-server (you, validating tokens), authorization-server (you, issuing tokens). Each has its own foot-guns.
- Resource-server (JWT validation):
- Issuer validation required:
spring.security.oauth2.resourceserver.jwt.issuer-uri or an explicit JwtDecoder with NimbusJwtDecoder.withIssuerLocation(issuer).
- Audience-claim validation must be built explicitly (not default).
OAuth2TokenValidatorFactories.create().andValidate(JwtIssuerValidator).andValidate(JwtAudienceValidator).
- Algorithm whitelist: accept
RS256 / ES256, reject HS256 unless explicitly intended. Algorithm confusion is a classic attack. See also api-security phase 2.
- Clock skew:
.setClockSkew(Duration.ofMinutes(2)) is reasonable, not 1 hour.
- Client (OAuth consumer):
- PKCE required for public clients (mobile, SPA that Spring only serves).
- Redirect URI strictly registered, no wildcards.
- Scopes minimal when requesting.
- Authorization-server:
- Spring Authorization Server is relatively young (stable since 2022). Use a vendor IdP (Keycloak, Auth0, Okta) unless you have a strong reason to self-host.
5. CVE patterns from recent years
The Spring ecosystem has a few infamous CVEs; each is a pattern to look for.
- Spring4Shell (CVE-2022-22965). Spring Framework RCE via class-loader manipulation in data binding. Patched in 5.2.20, 5.3.18. Historical but still relevant as a reviewer reflex: Java apps using
ServletRequestDataBinder without an allowlist are exposed. Spring Boot's default binder has been patched since the fix release.
- CVE-2022-22963 Spring Cloud Function — SpEL injection via the
spring.cloud.function.routing-expression header. Lesson: SpEL evaluation on untrusted input is RCE. Search your code for SpelExpressionParser().parseExpression(userInput).
- CVE-2023-20860 / -20861 Spring Framework — mass-binding and security bypass via
matchers combined with mvcMatchers. Fixed in 5.3.26, 6.0.7. Reviewer rule: mixing antMatchers and mvcMatchers is a foot-gun — use one consistently and prefer requestMatchers (Spring Security 6).
- CVE-2024-22257 Spring Security authorization bypass — possible bypass when
AuthenticatedVoter was configured without additional checks. [verify against https://spring.io/security/cve-2024-22257] for the exact patched versions in your context.
- More recent CVEs —
[verify against https://spring.io/security/] — check the CVE feed on every version bump or review. No invented IDs in findings.
6. Misc and verification-loop
@JsonIgnore on sensitive entity fields (password_hash, internal notes) to keep them out of JSON responses (mass-response, the mirror of mass-assignment).
@JsonProperty(access = WRITE_ONLY) for input-only fields.
- Use DTOs instead of serializing entities directly. Prevents accidental field exposure when the DB schema changes.
- Session fixation: Spring Security prevents this by default (
SessionAuthenticationStrategy). Do not turn it off without reason.
- Password hashing:
BCryptPasswordEncoder default. Argon2 variant via Argon2PasswordEncoder if the library is included. Never NoOpPasswordEncoder outside tests.
Verification-loop: Layer 1 (SecurityFilterChain config coherent? Actuator endpoints explicitly locked down? JWT issuer + audience both validated?), Layer 2 (CVE IDs confirmed via spring.io/security, OAuth flow names correct, no invented Spring annotations in examples).
Output
Spring Security review — <service/module>
Spring Boot: <x.y.z> | Spring Security: <x.y.z> | Version status: <current | N releases behind>
SecurityFilterChain:
Chains present: N
permitAll() matchers: <list + context>
.anyRequest() last: <authenticated | permitAll — FINDING>
CSRF status: <enabled | disabled with context>
CORS config: <scoped | wildcard — FINDING>
Method security:
@EnableMethodSecurity: <yes/no>
@PreAuthorize coverage: <controllers with/without>
Ownership checks: <present on resource endpoints?>
Actuator:
Exposure: <list of endpoints>
/env show-values: <NEVER | WHEN_AUTHORIZED | ALWAYS — FINDING>
/heapdump: <disabled | exposed — FINDING>
Separate port or filter:<yes/no>
OAuth2 / JWT:
Role: <client | resource-server | both>
Issuer validation: <yes/no>
Audience validation: <yes/no>
Algorithm whitelist: <yes/no>
CVE check:
Spring4Shell patched: <yes>
Recent security release:<within N days of upstream?>
cve-triage handoff: <N open>
Findings (severity-sorted, follow security-review format)
Verification-loop: ...
References
Categories
1---2name: spring-security3description: Spring Boot security review — Spring Security config (SecurityFilterChain), OAuth2/OIDC client and resource-server, method-level @PreAuthorize, JWT validation, actuator endpoint lockdown, CSRF model for web vs API, and recent Spring CVE patterns (Spring4Shell, SpEL injection, authorization bypasses).4---56# Spring Boot Security78## When to use910This skill is the Spring-specific layer on top of `secure-coding` and `api-security`. Spring Security is powerful and exactly therefore foot-gun-rich: small letters in the config decide whether your app is safe or wide open.1112Triggers on:1314- A question like "review our Spring Security config", "OAuth2 client setup", "lock down actuator endpoints", "JWT validation on a Spring Resource Server", "@PreAuthorize review".15- Presence of `spring-boot-starter-security`, `spring-security-oauth2-client`, `SecurityFilterChain` beans, `@EnableWebSecurity`, `@PreAuthorize`/`@PostAuthorize` annotations, `application.yml` with `spring.security.*` or `management.*`.16- A PR that touches `.permitAll()`, `.disable()` on CSRF/CORS, custom `AuthenticationProvider`, `UserDetailsService`, or a JWT filter.17- Spring version bumps around security releases.18- A handoff from `security-review` or `api-security` where Spring is in the stack.1920### When NOT (handoff)2122- General Java secure-coding → `secure-coding`.23- API design and OWASP API Top 10 — large overlap, but that skill is framework-agnostic. Use `api-security` for conceptual questions (what is IDOR, how do you validate a schema), this skill for Spring-specific implementation.24- SAST with Semgrep `p/java-spring` or CodeQL → `sast-orchestrator`.25- Dep vulns (incl. Log4Shell-class vulns in transitive deps) → `cve-triage`.26- Infra around Spring (Kubernetes deploy, container image) → `container-hardening` + `k8s-security`.27- Secrets (application.properties with DB passwords) → `secrets-scanner`.2829## Approach3031Six phases. Phase 1 (SecurityFilterChain) is where most production bugs live.3233### 1. SecurityFilterChain config3435Spring Security 6+ uses the Lambda DSL. Each chain decides which auth mode goes with which path and what "open" means.3637Common foot-guns:3839- **`.permitAll()` on too broad a path.** `http.authorizeHttpRequests(auth -> auth.requestMatchers("/api/**").permitAll())` opens the entire API. Look for `.permitAll()` on wildcards and challenge each one.40- **Order of matchers.** The filter chain matches top-down. A `/api/**` permitAll above a `/api/admin/**` authenticated rule overrules the latter. Always go from specific to general.41- **`.anyRequest().permitAll()`** — as the last rule this is catch-all open. Almost always wrong. The last rule should be `.anyRequest().authenticated()`, with an exception for a purely public app.42- **Multiple `SecurityFilterChain` beans** with `@Order`. First match wins. A too-broad first chain can render later chains redundant.43- **`.csrf(csrf -> csrf.disable())`** without context. CSRF on by default for stateful web auth. Disabling is correct for stateless JWT APIs, wrong for form-based auth. If in doubt: leave it on.44- **CORS config.** `.cors(cors -> cors.configurationSource(source))` with a `CorsConfigurationSource` that returns wildcards is a standard misconception. `allowedOrigins("*")` together with `allowCredentials(true)` does not work (Spring rejects it), but it is a signal that the config flow has not been thought through.4546Concrete reference config for a stateless JWT API:4748```java49@Bean50SecurityFilterChain api(HttpSecurity http) throws Exception {51 http52 .securityMatcher("/api/**")53 .authorizeHttpRequests(auth -> auth54 .requestMatchers(HttpMethod.GET, "/api/health").permitAll()55 .requestMatchers("/api/admin/**").hasRole("ADMIN")56 .anyRequest().authenticated())57 .oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()))58 .csrf(csrf -> csrf.disable()) // correct for stateless API59 .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));60 return http.build();61}62```6364### 2. Method-level authorization6566Route-level auth is never enough. Methods with sensitive logic deserve `@PreAuthorize` or equivalent.6768- **`@EnableMethodSecurity`** (Spring Security 6) replaces `@EnableGlobalMethodSecurity`. Without this annotation, `@PreAuthorize`/`@PostAuthorize` do not work.69- **`@PreAuthorize("hasRole('ADMIN')")`** — role-based.70- **`@PreAuthorize("hasAuthority('SCOPE_write:documents')")`** — for JWT scopes.71- **`@PreAuthorize("#id == authentication.name or hasRole('ADMIN')")`** — per-resource ownership, the fix for IDOR (see `api-security` API1 BOLA).72- **`@PostAuthorize`** — filter the return value after execution. Rarely needed, has performance impact.7374Common miss: ownership check in the service method but not in the controller, or vice versa. One place is enough, but it must be unambiguous which one.7576### 3. Actuator lockdown7778Spring Boot Actuator exposes operational endpoints. The default config (older) was wide open. Since Boot 2.x the defaults are `/health` and `/info` public, the rest authenticated. Reviewer rule: confirm you have not fallen back to the older model.7980- **`management.endpoints.web.exposure.include`** — what gets exposed. `"*"` is wrong in prod. Limit to what Ops actually needs: `health,info,prometheus,metrics`.81- **`management.endpoint.env.show-values=NEVER`** or `ALWAYS`/`WHEN_AUTHORIZED`. `/env` shows all env vars, including secrets when misconfigured.82- **`management.endpoint.heapdump.enabled=false`** in prod. Heap dump via HTTP is a memory-exfil primitive.83- **`management.server.port`** on a separate port that is not externally routable. Plus firewall/NetworkPolicy (see `k8s-security`).84- **Actuator security on the SecurityFilterChain**: a separate chain with `EndpointRequest.toAnyEndpoint()` matcher, role `ACTUATOR` or equivalent.8586### 4. JWT and OAuth28788Spring has three OAuth2 roles: client (consumer), resource-server (you, validating tokens), authorization-server (you, issuing tokens). Each has its own foot-guns.8990- **Resource-server (JWT validation)**:91 - **Issuer validation** required: `spring.security.oauth2.resourceserver.jwt.issuer-uri` or an explicit `JwtDecoder` with `NimbusJwtDecoder.withIssuerLocation(issuer)`.92 - **Audience-claim validation** must be built explicitly (not default). `OAuth2TokenValidatorFactories.create().andValidate(JwtIssuerValidator).andValidate(JwtAudienceValidator)`.93 - **Algorithm whitelist**: accept `RS256` / `ES256`, reject `HS256` unless explicitly intended. Algorithm confusion is a classic attack. See also `api-security` phase 2.94 - **Clock skew**: `.setClockSkew(Duration.ofMinutes(2))` is reasonable, not 1 hour.95- **Client (OAuth consumer)**:96 - **PKCE required** for public clients (mobile, SPA that Spring only serves).97 - **Redirect URI strictly** registered, no wildcards.98 - **Scopes minimal** when requesting.99- **Authorization-server**:100 - Spring Authorization Server is relatively young (stable since 2022). Use a vendor IdP (Keycloak, Auth0, Okta) unless you have a strong reason to self-host.101102### 5. CVE patterns from recent years103104The Spring ecosystem has a few infamous CVEs; each is a pattern to look for.105106- **Spring4Shell (CVE-2022-22965)**. Spring Framework RCE via class-loader manipulation in data binding. Patched in 5.2.20, 5.3.18. Historical but still relevant as a reviewer reflex: Java apps using `ServletRequestDataBinder` without an allowlist are exposed. Spring Boot's default binder has been patched since the fix release.107- **CVE-2022-22963 Spring Cloud Function** — SpEL injection via the `spring.cloud.function.routing-expression` header. Lesson: SpEL evaluation on untrusted input is RCE. Search your code for `SpelExpressionParser().parseExpression(userInput)`.108- **CVE-2023-20860 / -20861 Spring Framework** — mass-binding and security bypass via `matchers` combined with `mvcMatchers`. Fixed in 5.3.26, 6.0.7. Reviewer rule: mixing `antMatchers` and `mvcMatchers` is a foot-gun — use one consistently and prefer `requestMatchers` (Spring Security 6).109- **CVE-2024-22257 Spring Security authorization bypass** — possible bypass when `AuthenticatedVoter` was configured without additional checks. `[verify against https://spring.io/security/cve-2024-22257]` for the exact patched versions in your context.110- **More recent CVEs** — `[verify against https://spring.io/security/]` — check the CVE feed on every version bump or review. No invented IDs in findings.111112### 6. Misc and verification-loop113114- **`@JsonIgnore` on sensitive entity fields** (password_hash, internal notes) to keep them out of JSON responses (mass-response, the mirror of mass-assignment).115- **`@JsonProperty(access = WRITE_ONLY)`** for input-only fields.116- **Use DTOs** instead of serializing entities directly. Prevents accidental field exposure when the DB schema changes.117- **Session fixation**: Spring Security prevents this by default (`SessionAuthenticationStrategy`). Do not turn it off without reason.118- **Password hashing**: `BCryptPasswordEncoder` default. Argon2 variant via `Argon2PasswordEncoder` if the library is included. Never `NoOpPasswordEncoder` outside tests.119120Verification-loop: Layer 1 (SecurityFilterChain config coherent? Actuator endpoints explicitly locked down? JWT issuer + audience both validated?), Layer 2 (CVE IDs confirmed via spring.io/security, OAuth flow names correct, no invented Spring annotations in examples).121122## Output123124```125Spring Security review — <service/module>126Spring Boot: <x.y.z> | Spring Security: <x.y.z> | Version status: <current | N releases behind>127128SecurityFilterChain:129 Chains present: N130 permitAll() matchers: <list + context>131 .anyRequest() last: <authenticated | permitAll — FINDING>132 CSRF status: <enabled | disabled with context>133 CORS config: <scoped | wildcard — FINDING>134135Method security:136 @EnableMethodSecurity: <yes/no>137 @PreAuthorize coverage: <controllers with/without>138 Ownership checks: <present on resource endpoints?>139140Actuator:141 Exposure: <list of endpoints>142 /env show-values: <NEVER | WHEN_AUTHORIZED | ALWAYS — FINDING>143 /heapdump: <disabled | exposed — FINDING>144 Separate port or filter:<yes/no>145146OAuth2 / JWT:147 Role: <client | resource-server | both>148 Issuer validation: <yes/no>149 Audience validation: <yes/no>150 Algorithm whitelist: <yes/no>151152CVE check:153 Spring4Shell patched: <yes>154 Recent security release:<within N days of upstream?>155 cve-triage handoff: <N open>156157Findings (severity-sorted, follow security-review format)158Verification-loop: ...159```160161## References162163- Spring Security Reference — [https://docs.spring.io/spring-security/reference/](https://docs.spring.io/spring-security/reference/). Canonical docs, Lambda DSL and config patterns.164- Spring Security CVE feed — [https://spring.io/security/](https://spring.io/security/). All Spring projects CVEs, canonical source for verification.165- Spring Boot Actuator — [https://docs.spring.io/spring-boot/reference/actuator/index.html](https://docs.spring.io/spring-boot/reference/actuator/index.html). Endpoint config and security implications.166- OWASP Java Security Cheat Sheet — [https://cheatsheetseries.owasp.org/cheatsheets/Java_Security_Cheat_Sheet.html](https://cheatsheetseries.owasp.org/cheatsheets/Java_Security_Cheat_Sheet.html).167- Spring Framework Reference (RequestMapping, Binding) — [https://docs.spring.io/spring-framework/reference/](https://docs.spring.io/spring-framework/reference/). For patterns that include Spring4Shell-style attacks.168- RFC 8725 (JWT BCP) — [https://datatracker.ietf.org/doc/html/rfc8725](https://datatracker.ietf.org/doc/html/rfc8725). JWT-specific gotchas.169- OAuth 2.0 Security BCP — [https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics).170- NIST NVD — [https://nvd.nist.gov/](https://nvd.nist.gov/). For CVE verification on every cite.171172## Categories173174- appsec