Safeguard
Application Security

The most common Spring Boot security misconfigurations, and how to fix them

CVE-2026-40976 let anonymous users hit /actuator/env and /actuator/heapdump on default Spring Boot 4 filter chains, CVSS 9.1 — here's how to actually harden Spring Boot.

Safeguard Research Team
Research
6 min read

On April 23, 2026, the Spring team disclosed CVE-2026-40976, a CVSS 9.1 critical flaw affecting Spring Boot 4.0.0 through 4.0.5. The bug is almost embarrassingly simple: on a servlet-based application that never wrote a custom Spring Security configuration and relies on Spring Boot's own default filter chain, the default chain silently failed to enforce authorization on actuator endpoints whenever spring-boot-actuator-autoconfigure was on the classpath without spring-boot-health. The practical result was that /actuator/env, /actuator/heapdump, and /actuator/configprops were reachable by anonymous requests — and a heap dump plus an environment listing is frequently enough on its own to pull database credentials, signing keys, and session tokens straight out of memory. It's the latest entry in a pattern that goes back years: Spring Boot ships secure-by-default primitives, but the defaults only hold if you don't accidentally opt out of them. This piece walks through the three misconfiguration classes that keep showing up in Spring Boot applications — actuator exposure, unsafe deserialization, and default credentials — and how to close each one using Spring Security's own documented controls, not folklore.

Why do exposed actuator endpoints keep causing breaches?

Actuator endpoints exist to expose operational internals — environment variables, heap dumps, thread dumps, live logger levels — and that is exactly what makes them dangerous when authorization slips. CVE-2026-40976 is the sharpest recent example: because the vulnerable condition depended on the absence of a custom security configuration, teams following the "just add the starter and go" path were the ones most exposed, while teams that had already written explicit SecurityFilterChain beans were unaffected. It sits alongside CVE-2026-22731 and CVE-2026-22733, both involving improper mapping of application endpoints under the same sensitive infrastructure paths Actuator uses. This isn't new territory — security researchers and OWASP guidance have flagged unauthenticated /actuator/* exposure for years, and endpoints like /actuator/jolokia and /actuator/heapdump have been chained into full remote code execution in real incidents when combined with logging or env-mutation endpoints. The lesson is consistent: actuator's value as a debugging surface is inversely proportional to how safe it is to leave open.

How do you actually lock down actuator in production?

Start from Spring Boot's own management.endpoints.web.exposure property, which defaults to exposing only health and info over HTTP — everything else must be explicitly allowlisted, not blocklisted. If your project (or a starter you inherited) sets include: "*", that single line is the whole vulnerability; replace it with a named list of only the endpoints you actually consume. For anything you do expose beyond health/info, put it behind Spring Security explicitly: map EndpointRequest.toAnyEndpoint() to a hasRole("ACTUATOR_ADMIN") rule in your SecurityFilterChain, rather than assuming the default chain covers it — which is precisely the assumption CVE-2026-40976 broke. Disable endpoints you never use per-endpoint with management.endpoint.<id>.enabled=falseheapdump, env, shutdown, and jolokia are the highest-value targets for that treatment. Finally, run actuator on a separate management port (management.server.port) bound to an internal interface, so even a filter-chain regression doesn't put it on the public listener at all.

What made CVE-2022-22965 (Spring4Shell) different from a normal deserialization bug?

Spring4Shell, tracked as CVE-2022-22965, went from zero-day exploit code circulating publicly on March 30, 2022, to a patched release on March 31, 2022 — a 24-hour turnaround that reflects how severe the CVSS 9.8 rating was. It affected Spring Framework 5.3.0 through 5.3.17 and 5.2.0 through 5.2.19, fixed in 5.3.18 and 5.2.20. Unlike classic Java deserialization RCEs that abuse ObjectInputStream on attacker-controlled byte streams, Spring4Shell was a data-binding flaw: on JDK 9+, deployed as a WAR to Tomcat, with spring-webmvc or spring-webflux on the classpath, an attacker could manipulate a request's parameter names to reach the ClassLoader through property binding on Class objects, ultimately writing a malicious JSP to the web root. It's grouped with deserialization-class RCEs in most vendor advisories because the impact and remediation urgency were identical, even though the mechanism ran through Spring's binder rather than raw object deserialization. The fix disallowed binding to Class-typed properties by default — teams still on affected point releases in 2026 remain exploitable with public PoCs available.

How should you actually prevent deserialization-class RCEs in Spring apps?

First, patch: there is no configuration workaround for Spring4Shell that beats simply running Spring Framework 5.3.18+/5.2.20+ or any current Spring Boot 3.x/4.x release, since the vulnerable data-binding path was removed at the framework level. Second, treat any endpoint that deserializes untrusted input — via ObjectInputStream, Jackson polymorphic typing, or XML unmarshalling — as a taint source: Jackson's enableDefaultTyping() (deprecated for good reason) and unrestricted @JsonTypeInfo configurations have produced their own RCE chains independent of Spring4Shell, so pin PolymorphicTypeValidator allowlists rather than accepting arbitrary class names from JSON. Third, if you deploy to Tomcat as a WAR rather than an embedded, executable JAR, you inherit a larger attack surface for binder-based bugs like Spring4Shell — Spring's own guidance has increasingly favored the embedded-server model specifically because it removes classes of container-level exploitation. None of this replaces dependency scanning: knowing your resolved Spring Framework version against the CVE record is still the fastest way to know if you're exposed at all.

Why do default credentials still show up in Spring Boot apps that "have security enabled"?

Adding spring-boot-starter-security to a project without further configuration generates an in-memory user named user with a random password printed once to the console log at startup — a safety net for local development that becomes a liability the moment it survives into a shared or production environment via a leaked log line or a hardcoded override of spring.security.user.password. The H2 database console is the other repeat offender: spring.h2.console.enabled=true ships with default credentials and, if left reachable outside local development, gives an attacker a SQL console directly against your datastore. Spring Boot Admin, a popular actuator-visualization dashboard, has shipped with no authentication by default in several deployments teams have documented publicly, exposing the same actuator data actuator hardening is meant to protect. The fix in all three cases is the same discipline: define real UserDetailsService and PasswordEncoder beans per the official Spring Security reference documentation, gate the H2 console behind a profile that never activates outside dev, and put Spring Boot Admin's own UI behind the same SecurityFilterChain review you apply to actuator itself.

How Safeguard helps

None of these misconfigurations are exotic — they're documented in Spring's own security advisories and reference docs, which is exactly why they keep recurring: the fix is a configuration review, not a patch, and configuration reviews get skipped under deadline pressure. Safeguard's software composition analysis continuously resolves the Spring Framework and Spring Boot versions actually running in your dependency tree against the CVE record, so a lingering Spring Framework 5.3.17 or an unpatched Spring Boot 4.0.x shows up as a ranked finding instead of something a team has to remember to check manually after the next advisory drops. Paired with SBOM generation on every build, that gives a security team a queryable answer to "are we running an affected version of Spring Boot" in minutes rather than days — the same operational gap that turns a 24-hour patch window like Spring4Shell's into a much longer real-world exposure window for teams without that visibility.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.