Spring Boot Security Review
Credit: This skill builds on the work of the original open-source author.
Use this skill when you add or review:
- Login or token code
- Access rules
- Web routes
- User input
- File uploads
- Secrets
- Security settings
Review Steps
- Find each public route.
- Check who may call each route.
- Check all input before use.
- Check tokens, cookies, and CSRF rules.
- Check SQL, files, logs, and secrets.
- Check limits, headers, and libraries.
- Report each risk with a clear fix.
Deny access unless a rule allows it. Give each user only the access they need.
Login and Tokens
Prefer Spring Security OAuth2 Resource Server for bearer tokens. Do not write token checks by hand unless the app has a clear need.
For JWTs, check:
- The signature
- The allowed signing method
- The issuer
- The audience
- The expiry time
- The not-before time
Reject bad, missing, expired, or revoked tokens. Do not trust claims before all checks pass.
If the app needs fast token revoke, use short-lived JWTs with refresh token revoke, or use opaque tokens with a revoke list.
For login sessions, use cookies with:
HttpOnlySecureSameSite=LaxorSameSite=Strict
Use SameSite=None only when cross-site use is needed. It also needs Secure.
Do not place tokens in URLs. URLs may leak into logs and browser history.
Example resource server setup:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/health").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build();
}If a custom filter is required:
- Extend
OncePerRequestFilter. - Accept only the expected token format.
- Do not set login state until all checks pass.
- Return
401for a bad login. - Clear login state when a request fails.
- Do not log the token.
Access Rules
Enable method checks:
@EnableMethodSecurity
@Configuration
class SecurityConfig {
}Use clear rules:
@PreAuthorize("hasRole('ADMIN')")@PreAuthorize("@authz.canEdit(authentication, #id)")Check access to the exact record being read or changed. A logged-in user must not gain access by changing an ID in the URL.
Use:
401 Unauthorizedwhen login is missing or bad403 Forbiddenwhen login is valid but access is denied
Do not trust a role, user ID, price, or owner ID sent by the client.
Input Checks
Use @Valid on request data:
public record CreateUserRequest(
@NotBlank @Size(max = 80) String name,
@NotBlank @Email @Size(max = 254) String email
) {
}
@PostMapping("/users")
public ResponseEntity<Void> create(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).build();
}Set limits for:
- Text length
- List size
- Number range
- Date range
- Nested object depth
- Request body size
Reject unknown fields when they may hide client errors.
Validation does not make HTML safe. If users may enter HTML, clean it with a strict allow list before showing it. Escape plain text when it is shown on a page.
Return safe error messages. Do not show stack traces, SQL text, file paths, or secret values.
SQL Safety
Use Spring Data or queries with bound values.
Safe:
@Query("select u from User u where u.email = :email")
Optional<User> findByEmail(@Param("email") String email);Unsafe:
String sql = "select * from users where email = '" + email + "'";Never build SQL field names, sort rules, or table names from raw user input. Map user choices to a fixed allow list.
CSRF
Keep CSRF protection on when a browser sends login cookies by itself. This includes session apps.
Put the CSRF token in each form or approved request header.
CSRF may be turned off only when all of these are true:
- The app is a stateless API.
- Login uses a bearer token in the
Authorizationheader. - The browser does not send login cookies.
- No route also accepts cookie login.
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));CORS is not a CSRF fix. Use a short allow list for trusted origins. Do not use * with login data.
Passwords and Account Safety
If the app stores passwords:
- Hash them with Spring Security's
PasswordEncoder. - Prefer Argon2id or bcrypt.
- Never store or log plain passwords.
- Use the same reply for unknown users and wrong passwords.
- Limit login and password reset tries.
- Make reset tokens random, short-lived, and single-use.
- End old sessions after a password reset when needed.
Do not use security questions as the main reset method.
Secrets
- Never put secrets in source code.
- Never commit secrets to Git.
- Read secrets from environment values or a secret store.
- Keep real values out of
application.yml. - Use placeholders such as
${DB_PASSWORD}. - Give each secret only the access it needs.
- Change a secret at once if it may have leaked.
- Plan secret changes so old and new values can overlap for a short time.
Also check test files, sample files, shell scripts, build logs, and Git history.
Security Headers
Keep Spring Security header defaults. Add a content policy that fits the app.
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; object-src 'none'; frame-ancestors 'self'"))
.frameOptions(frame -> frame.sameOrigin())
.contentTypeOptions(Customizer.withDefaults())
.referrerPolicy(referrer -> referrer
.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));Test the content policy. A rule that breaks the app may be removed later and leave no guard.
Use HTTPS in live systems. Add HSTS only when the site and its subdomains are ready for HTTPS.
Rate Limits
Limit routes that are costly or easy to abuse, such as:
- Login
- Password reset
- Sign-up
- Search
- File upload
- Email or message send
- Large reports
Use Bucket4j or a trusted gateway.
Set limits by user when logged in. Use IP limits as a second guard. Trust forwarded IP headers only from known proxies.
Return 429 Too Many Requests. Add Retry-After when the wait time is known.
Do not log every blocked try. This can flood logs. Count or group repeat events.
File Uploads
Check:
- File size
- File count
- File name length
- File type
- File content, not just the stated type
- Allowed file extensions
Also:
- Make a new random file name.
- Do not use the user path.
- Block
..and path tricks. - Store files outside the web root.
- Do not run uploaded files.
- Scan files when the risk calls for it.
- Set safe download headers.
- Remove file metadata when it may hold private data.
Images and archive files may expand to a huge size. Set limits after decode or unpack too.
Logs and Private Data
Never log:
- Passwords
- Tokens
- Session IDs
- Secret keys
- Full card numbers
- Full request bodies by default
Hide or remove private fields before logging. Limit who can read logs and how long logs are kept.
Log key security events, such as denied access and account changes. Do not include secret data in those events.
Library Safety
- Keep Spring Boot, Spring Security, Java, and build tools on supported versions.
- Run a library risk scan in CI.
- Fail the build for known high-risk flaws unless a time-bound exception is approved.
- Review indirect libraries too.
- Remove libraries that are not used.
- Do not update a major version without tests.
Concrete Usage Example
User request:
Review POST /accounts/{id}/email. It uses a session cookie and accepts a new email address.Apply this skill:
- Confirm login is required.
- Check that the user owns
id, or has an allowed admin role. - Do not trust an owner ID from the request body.
- Use
@Valid,@Email, and a length limit. - Keep CSRF on because the route uses a session cookie.
- Use a bound query when saving the email.
- Do not log the email change request body.
- Rate-limit repeat changes if the route sends a message.
- Return
403for a valid user who cannot edit the account. - Add tests for missing login, bad CSRF token, bad email, wrong owner, and valid owner.
A useful review result is:
High: The route checks only that the user is logged in. Any user can change another
account by changing the path ID.
Fix: Check ownership or admin rights before the update.
Test: User A must get 403 when changing User B's email.Tests to Add
Test both success and failure cases:
- No login
- Bad or expired token
- Wrong role
- Wrong record owner
- Missing CSRF token
- Bad input
- Very large input
- SQL control text in input
- Too many requests
- Unsafe upload name
- Wrong upload content
- Safe error response
Release Checklist
- Every private route requires login.
- Every record has an owner or role check.
- Tokens have full checks and expire.
- Session cookies have safe flags.
- CSRF rules match the login type.
- CORS has a short allow list.
- All input has size and value limits.
- SQL uses bound values.
- Passwords use a safe hash.
- Secrets are outside source code and logs.
- Security headers are set and tested.
- Risky routes have rate limits.
- Uploads have type, size, name, and path checks.
- Errors do not leak private details.
- Logs do not hold secret or private data.
- Libraries are scanned and supported.
- Access checks have failure tests.