Spring Boot's whole value proposition is that it decides things for you. Auto-configuration, starter dependencies, and embedded servers let a team ship in days instead of weeks. That same "it just works" magic is a security liability when the defaults expose more than you intended and the dependency graph pulls in code you never audited. Spring4Shell (CVE-2022-22965, CVSS 9.8) was a stark reminder: a data-binding feature working exactly as designed became remote code execution on affected deployments. This guide is about the specific places Spring Boot's conveniences need hardening, with the configuration to do it.
What is the biggest Spring Boot security risk in practice?
Two categories dominate real incidents: exposed operational endpoints (Actuator) and unpatched framework dependencies. Actuator ships powerful introspection and management endpoints, and misconfigured exposure has leaked heap dumps, environment variables, and credentials to the public internet. Meanwhile, a Spring Boot app is a large transitive dependency tree — a single spring-boot-starter-web drags in Tomcat, Jackson, and Spring MVC, each with its own CVE history. So the first move is not writing more security code; it's constraining what auto-configuration exposes and knowing what's on your classpath.
Lock down Actuator
By default in recent Spring Boot versions only /actuator/health is exposed over HTTP, but teams routinely widen this to * for debugging and forget to revert. Be explicit and minimal:
management:
endpoints:
web:
exposure:
include: health,info # never "*"
endpoint:
health:
show-details: when-authorized
server:
port: 9001 # separate management port
Then require authentication on the management endpoints and, ideally, bind them to an internal-only port that never touches your public ingress. A heap dump behind /actuator/heapdump is a credential exfiltration primitive if it's reachable.
Enforce security headers and HTTPS
Spring Security applies sensible header defaults, but you should still assert them so a config drift doesn't silently drop protections. Redirect to HTTPS and set HSTS:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.requiresChannel(c -> c.anyRequest().requiresSecure())
.headers(h -> h
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000))
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'")));
return http.build();
}
Content-Security-Policy is off by default — set it explicitly, because it's your strongest defense-in-depth against reflected XSS.
Keep starters patched, and prefer the BOM
Spring Boot's spring-boot-dependencies BOM curates compatible, patched versions of everything it manages. Let it — override versions only with a specific reason, because pinning one library to an old version to "keep things stable" is how CVE-2022-22965-class exposure lingers. Inherit the BOM and keep the Boot version current:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
The Spring4Shell fix landed in Spring Framework 5.3.18/5.2.20 and Spring Boot 2.6.6/2.5.12; the related Spring Cloud Function SpEL flaw (CVE-2022-22963) was a separate patch. The lesson isn't "memorize these versions" — it's that staying near the current release line is the cheapest security control you have.
Don't ship development conveniences to production
Several defaults that speed up local work are dangerous in production. Disable the H2 web console — its JNDI-backed connection handling was exploitable (CVE-2021-42392) — and never enable spring-boot-devtools in a production artifact. Turn off verbose error responses that leak stack traces:
server:
error:
include-stacktrace: never
include-message: never
spring:
h2:
console:
enabled: false
Validate and bind request data carefully
Use Bean Validation on your DTOs and avoid binding directly to entity or domain objects that expose fields a client should never set. Mass-assignment style bugs happen when a @RequestBody maps onto an object with an isAdmin or roles field. Use a dedicated request DTO with only the fields the endpoint accepts, and annotate constraints:
public record CreateUserRequest(
@Email String email,
@Size(min = 12, max = 128) String password) {}
Then apply @Valid at the controller boundary so constraints are actually enforced, and return a generic validation error rather than echoing the offending value back to the client.
Externalize secrets, and keep them out of the image
Spring Boot makes it dangerously easy to drop a database password straight into application.properties and commit it. Don't. Reference secrets through externalized configuration resolved at runtime — environment variables at minimum, a secrets manager (Vault, AWS Secrets Manager) ideally — and never bake them into a container layer, because image history preserves the value even after you "remove" it. Spring Boot's support for config trees and cloud secret backends means you rarely need a plaintext secret in the repository at all. Treat any credential that has ever been committed as compromised: rotate it, then scrub the history.
Spring Boot hardening checklist
- Actuator exposure limited to
health,info, authenticated, on an internal port - Boot version current; dependency versions inherited from the BOM, not pinned old
- CSP and HSTS asserted explicitly; all traffic over HTTPS
- H2 console and devtools disabled in production; stack traces suppressed
- Request DTOs distinct from domain entities; Bean Validation enforced
- Every build scanned for vulnerable transitive dependencies
How Safeguard helps
The recurring theme above is that Spring Boot's risk lives in the dependency graph and in configuration you can't eyeball across dozens of services. Safeguard's software composition analysis inventories every transitive dependency a starter pulls in — the Tomcat, Jackson, and Spring MVC versions you never declared — and flags the ones with known CVEs. Its reachability engine then tells you whether a flagged path like the Spring4Shell data-binding route is actually reachable in your service, so you can distinguish an urgent fix from theoretical noise. When a safe upgrade exists, auto-fix pull requests bump the version against your repo with the change already applied, and you can run the same scan pre-commit through the Safeguard CLI. If you're comparing tooling, the Safeguard vs Snyk comparison covers how the two approaches differ on transitive-dependency visibility.
Create a free account at app.safeguard.sh/register, and see the Spring integration walkthrough at docs.safeguard.sh.