Enterprise Java applications fail less often because of the language and more often because of a framework default someone changed, a dependency nobody tracked or a management endpoint left open. Log4Shell and Spring4Shell showed how one library flaw can reach thousands of systems at once, and both remain in the CISA Known Exploited Vulnerabilities catalogue. This guide covers the parts of a Java and Spring estate that decide whether an application holds up: Spring Security configuration, Spring Boot Actuator, deserialisation and expression injection, dependency patching, keystores and the credentials Java services carry. It is written for application security engineers, platform teams and developers who own Spring Boot services in production, and it assumes you already know the basics of web application risk.
Key takeaways
- Deny by default, then open up. Define every
SecurityFilterChaindeliberately, end it with a catch-all rule and add method security for object-level checks, because unannotated methods are not protected. - Actuator is an admin interface. Only
healthis exposed over HTTP by default; every endpoint you add tomanagement.endpoints.web.exposure.includeneeds authentication, a role and ideally a separate port or network path. - Untrusted data must never choose a class or an expression. Java deserialisation, SpEL, JNDI lookups and XML external entities all turn input into behaviour; filter with
jdk.serialFilterand keep input out of evaluated expressions. - Your real attack surface is the dependency tree. Log4j and Spring Framework flaws were exploited within days of disclosure, so an SBOM, a remediation time frame and the ability to rebuild every service quickly matter more than any single control.
- Keystores and service credentials are identities. JKS and PKCS12 files, database passwords in
application.propertiesand tokens in environment variables belong in a secrets manager with owners and rotation.
The Java and Spring attack surface
A typical enterprise Java service is a Spring Boot application on a JVM, packaged as an executable jar or a WAR on a servlet container such as Apache Tomcat, pulling in dozens to hundreds of transitive dependencies. Each layer has its own class of failure.
| Layer | Typical weakness | Known exploited example (CISA KEV) | Primary control |
|---|---|---|---|
| Logging library | Attacker-controlled strings evaluated as lookups | CVE-2021-44228, Apache Log4j2 remote code execution (added 10 December 2021, known ransomware use) | Upgrade, SBOM-driven inventory, egress filtering from application servers |
| Web framework data binding | Request parameters bound to unintended object properties | CVE-2022-22965, Spring Framework JDK 9+ remote code execution (added 4 April 2022) | Patch, restrict bindable fields, prefer executable jar packaging |
| Expression evaluation | User input reaching SpEL or other expression engines | CVE-2022-22963, Spring Cloud Function remote code execution (added 25 August 2022) | Never evaluate input as an expression; patch |
| Identity middleware | Legacy, internet-facing Java middleware left unpatched | CVE-2021-35587, Oracle Fusion Middleware (added 28 November 2022) | Patch or retire; decommission legacy login servers fully |
| Serialisation | Native Java deserialisation of untrusted streams (CAPEC-586 Object Injection) | Many gadget-chain CVEs across libraries | ObjectInputFilter allowlists, avoid native serialisation at trust boundaries |
| Management plane | Actuator endpoints such as env or heapdump reachable without authentication | Configuration error rather than a CVE | Minimal exposure, Spring Security on EndpointRequest.toAnyEndpoint() |
| Secrets and keystores | Passwords in properties files, default keystore passwords, keys in images | Configuration error | Secrets manager, PKCS12 with unique passwords, rotation |
Insecure deserialisation has its own guide on this site, so this page covers only the Java-specific controls. See the Insecure Deserialisation Guide.
Spring Security configuration that holds up
Request-level authorisation
- Own the filter chain: once you declare your own
SecurityFilterChainbean, Spring Boot's default security auto-configuration backs off. That is intended, but it means anything you forget to cover is no longer covered by a default. Review every chain as code. - End with a catch-all: the Spring Security documentation recommends a final rule in
HttpSecurityso every request matches at least one rule. In practice that isanyRequest().authenticated()oranyRequest().denyAll()after the explicit permits, neveranyRequest().permitAll(). - Order matters: matchers are evaluated in order, so a broad
permitAll()early in the chain silently overrides stricter rules below it. Put specific paths first and test the negative cases.
Method security for object-level checks
- Turn it on explicitly:
@EnableMethodSecurityis not enabled by the Spring Boot security starter. It enables@PreAuthorize,@PostAuthorize,@PreFilterand@PostFilter;@Securedand JSR-250 annotations needsecuredEnabledorjsr250Enabled, and the documentation recommends@PreAuthorizeover both. - Use it for ownership: expressions such as
returnObject.owner == authentication.nameare the documented way to stop insecure direct object references. Do not use@PostAuthorizeon methods that write, because the change happens before the check runs. - Know the proxy limit: method security is built on Spring AOP proxies, so a call from one method to another on the same bean skips the proxy and the second method's annotation is not evaluated. Put authorisation on the public entry point or move the protected method to a separate bean.
- Unannotated means unprotected: method security only applies where annotations exist. Keep request-level rules as the coarse layer and method rules as the fine layer, not one instead of the other.
CSRF, cookies and sessions
- Keep CSRF protection on for browsers: the Spring Security documentation says to disable it only for services used exclusively by non-browser clients. A JSON API that accepts a session cookie or HTTP Basic credentials from a browser is still exposed, including to
enctype="text/plain"form posts, so validateContent-Typeas well. - Keep safe methods safe: GET, HEAD, OPTIONS and TRACE must not change state, because both the synchroniser token pattern and SameSite protection depend on it.
- Set SameSite yourself: Spring Security does not set the SameSite attribute on the session cookie; Spring Session can. Use
LaxorStrictwithSecureandHttpOnlyas defence in depth, not as the only CSRF control.
Password storage
- Use DelegatingPasswordEncoder: it is the default, stores hashes as
{id}encodedPasswordand lets you add a stronger algorithm later while still verifying old hashes. The default encoder is bcrypt with strength 10. - Tune the work factor: the documentation recommends about one second per verification on your hardware, then exchanging the password for a short-lived session or token rather than checking it on every request.
- Never ship NoOpPasswordEncoder: it stores plain text. To migrate legacy hashes, prefix them with the correct id rather than falling back to no encoding.
- Choose by constraint: PBKDF2 is the documented choice where FIPS certification is required; Argon2 and scrypt are memory-hard alternatives (Argon2 needs BouncyCastle).
Spring Boot Actuator and production profiles
Actuator is the most common way a well-written Spring Boot application leaks configuration. Treat every endpoint beyond health as an administrative API.
| Property or endpoint | Default (Spring Boot 4.x documentation) | Recommended production setting |
|---|---|---|
management.endpoints.web.exposure.include | health | List endpoints explicitly; never * on an internet-facing port |
management.endpoints.web.exposure.exclude | Empty (exclude wins over include) | Exclude env, configprops, heapdump, threaddump, beans, logfile unless needed |
management.endpoints.access.default | Unrestricted, except shutdown and heapdump | none, then opt in per endpoint with management.endpoint.<id>.access |
management.endpoints.access.max-permitted | Not set | read-only in production so no endpoint can be made writable by accident |
management.endpoint.env.show-values (also configprops, quartz) | Values sanitised to ****** | never, or when-authorized with a named role in management.endpoint.env.roles |
heapdump | Web only, access restricted | Leave restricted; a heap dump can contain secrets, tokens and session data in memory |
shutdown | Disabled, jar packaging only | Leave disabled |
- Secure the endpoints with their own chain: if Spring Security is present and you define no chain, Boot secures every actuator endpoint except
/health. Once you define your own chain, add one scoped withsecurityMatcher(EndpointRequest.toAnyEndpoint())that requires a dedicated role. - Separate the network path: run management endpoints on a different port or behind an internal-only route so the public load balancer never forwards
/actuator/**. - Use profiles carefully: Spring profiles such as
devoften enable H2 consoles, debug logging, permissive CORS or wide actuator exposure. Fail the build or start-up if a development profile is active in production, which also satisfies OWASP ASVS 5.0 V13.4.2 (debug modes disabled in production). - Hide versions: remove server and framework version banners and error-page stack traces (ASVS V13.4.6), so attackers cannot match you to a CVE list in one request.
Deserialisation, expressions and lookups
The most damaging Java flaws share one pattern: input that should have been data was treated as instructions.
- Native deserialisation: avoid
ObjectInputStreamon any data that crosses a trust boundary. Where you cannot, set a filter. JEP 290 (JDK 9) addedObjectInputFilterand the process-widejdk.serialFiltersystem or security property. Patterns combine limits (maxdepth,maxrefs,maxbytes,maxarray) with class rules, wherecom.example.dto.**accepts a package tree and a leading!rejects. End an allowlist with!*so anything unlisted is rejected. Filter decisions are logged to thejava.io.serializationlogger, which is useful in audit mode. ASVS V1.5.2 requires an allowlist of object types for untrusted deserialisation. - Polymorphic JSON and XML: the same risk returns when a JSON or XML library lets the payload name the class to instantiate. Disable default typing, bind to concrete DTOs and configure XML parsers so external entities are off (ASVS V1.5.1). See the XXE Guide.
- Expression languages: SpEL, OGNL and template engines are code. Never build an expression string from request data, and keep user input out of
@PreAuthorizeexpressions, routing headers and dynamic queries. See the Server-Side Template Injection (SSTI) Guide. - JNDI and lookups: Log4Shell worked because Log4j evaluated
${jndi:...}lookups in log messages and could load code from an attacker-controlled LDAP server. Beyond patching, deny outbound LDAP, RMI and arbitrary HTTP from application servers so a future lookup flaw has nowhere to call. Encode logged data to prevent log injection (ASVS V16.4.1). - Data binding: Spring4Shell abused data binding to reach class loader properties. Bind requests to purpose-built DTOs, restrict bindable fields with
@InitBinderandsetAllowedFieldswhere you must bind to domain objects, and avoid binding directly to JPA entities.
Dependencies and patching: what Log4Shell and Spring4Shell taught
| Vulnerability | Affected | Fixed in | Conditions and notes |
|---|---|---|---|
| CVE-2021-44228 (Log4Shell) | Log4j 2.0-beta9 to 2.14.x, other than the fixed 2.12.2 and 2.3.1 releases | 2.15.0 (Java 8), 2.12.2 (Java 7), 2.3.1 (Java 6) | JNDI lookups in configuration, log messages and parameters |
| CVE-2021-45046 | Incomplete fix for 44228 | 2.16.0 (Java 8), 2.12.3 (Java 7), 2.3.1 (Java 6) | Non-default Pattern Layouts with Thread Context lookups |
| CVE-2021-45105 and CVE-2021-44832 | Later Log4j 2 releases | Current Log4j 2.x release | Denial of service through recursive lookups; JDBC Appender JNDI risk requiring write access to logging configuration |
| CVE-2022-22965 (Spring4Shell) | Spring Framework 5.3.0 to 5.3.17, 5.2.19 and earlier | 5.3.18, 5.2.20 | JDK 9+, Tomcat, WAR deployment, spring-webmvc or spring-webflux; executable jars not exposed to the published exploit, but Spring warned the underlying issue was more general |
- Patch in waves, not once: Log4j needed several releases in quick succession because the first fixes were incomplete.
- Know where the jar is: Log4j was often a transitive dependency, shaded inside another jar or bundled in a vendor appliance. Maintain an SBOM per build (ASVS V15.1.2), scan images and file systems, not just
pom.xml, and include third-party Java products in the inventory. See the SBOM Guide. - Set remediation time frames: ASVS V15.1.1 asks for documented, risk-based time frames for vulnerable components and V15.2.1 for proof the application meets them. Treat a KEV listing as the top tier. See the Vulnerability Prioritisation Guide.
- Stay on supported lines: unsupported Spring, Tomcat and JDK versions stop receiving public fixes. Track end-of-support dates for each line you run and budget the major upgrades (for example javax to jakarta namespaces) before the deadline, not after.
- Control where artefacts come from: resolve dependencies through an internal repository proxy so every component comes from an expected source (ASVS V15.2.4). See the Malicious Package and Dependency Confusion Guide.
Where identity fits: keystores, secrets and service credentials
Every Java service is a non-human identity. It holds database passwords, API keys, OAuth client secrets and private keys, usually more than any human user of the system.
- Keystores: since JDK 9 (JEP 229) new keystores are PKCS12 by default; existing JKS files keep working. Give each keystore a unique, generated password stored in a secrets manager, never the well-known default of the JDK
cacertstruststore, and never bake private keys into container images. - Properties files: passwords in
application.properties,application.ymlor profile-specific files end up in Git, images and Actuatorenvoutput. Load them at runtime from a vault or a cloud secrets service (ASVS V13.3.1). See the Secrets Management Guide. - Workload identity over static keys: where the platform supports it, let the service authenticate to cloud APIs and databases with short-lived workload credentials rather than long-lived keys. See the NHI Authentication Guide.
Choosing controls: decision guidance
| Decision | Choose A when | Choose B when |
|---|---|---|
| Executable jar vs WAR on a shared servlet container | Jar: you want one patched runtime per service and smaller blast radius | WAR: a platform team runs a hardened, centrally patched container and you can keep it current |
| Request rules vs method security | Request rules: coarse access by path, role or authentication state | Method security: ownership and object-level checks that depend on the data returned |
| bcrypt vs PBKDF2 vs Argon2 | bcrypt: the default, widely supported | PBKDF2 for FIPS requirements; Argon2 where memory-hard hashing is wanted and BouncyCastle is acceptable |
| Native serialisation vs explicit formats | Native, filtered: legacy RMI or caching between trusted components only | JSON or protobuf bound to concrete types: anything crossing a trust boundary |
Small teams: standardise on one Spring Boot baseline (a parent POM or Gradle convention plugin) that sets actuator exposure, security defaults and dependency versions, then let dependency update bots raise pull requests. Large estates: add a central SBOM store, a policy that blocks builds with KEV-listed components, a shared library of approved SecurityFilterChain patterns and an inventory of every Java product bought from vendors, because those were the slowest to patch during Log4Shell.
How to implement: a phased approach
- Inventory (weeks 1 to 2): list every Java service and product, its JDK, Spring and container versions, packaging type and internet exposure. Generate SBOMs for your own builds and scan file systems and images for bundled jars.
- Close the urgent gaps: patch KEV-listed components first, remove actuator exposure beyond
healthon public paths and block outbound LDAP and RMI from application servers. - Set the baseline: publish a shared configuration with a deny-by-default filter chain, method security enabled,
management.endpoints.access.default=none, sanitisedenvvalues and a production profile guard. - Test in audit mode: deploy a
jdk.serialFilterwith logging to a pilot service and review thejava.io.serializationlog for classes you need before switching to reject. Run negative authorisation tests (anonymous, wrong role, other user's object) in CI. - Roll out in rings: internal services first, then lower-risk external services, then critical ones. Keep the previous configuration version tagged so a rollback is a redeploy, not a rewrite.
- Move secrets out: migrate passwords and keys from properties files and images to a secrets manager, rotate each one as you move it, and remove the old values from Git history where possible.
- Keep it current: enforce remediation time frames in the pipeline, review end-of-support dates quarterly and rehearse a "new critical library CVE" drill once a year.
Detection and measurement
- Log authorisation failures: Spring Security
AuthorizationDeniedEventand authentication failure events, forwarded to your SIEM with user, path and source. - Watch the management plane: alert on any request to
/actuator/**from outside the management network and onheapdumporenvaccess at all. - Watch egress: outbound LDAP, RMI or unexpected HTTP from application servers was the clearest Log4Shell signal and remains a good indicator of injection.
- Watch for web shells and new processes: a JVM spawning shells or writing JSP files to the web root suggests exploitation (ATT&CK T1190 followed by T1505.003).
- Log deserialisation rejections: filter rejections in production are either a missing allowlist entry or an attack, and both need attention.
| Metric | What good looks like |
|---|---|
| Services with an SBOM generated on every build | 100% of in-house Java services |
| Median time to remediate a KEV-listed Java component | Within your documented top-tier time frame, measured from KEV addition |
Internet-reachable actuator endpoints other than health | Zero, verified by external scan |
| Services on an unsupported Spring, Tomcat or JDK line | Falling each quarter, with an upgrade date for every one |
| Plain-text secrets found in properties files and images | Zero new findings; legacy findings rotated and removed |
| Endpoints covered by negative authorisation tests | All state-changing and object-returning endpoints |
Common failure patterns
- A custom filter chain that forgot the catch-all: a new controller added later is reachable without authentication because no rule matched it.
exposure.include=*copied from a tutorial:env,configpropsandheapdumpbecome reachable, and a heap dump hands over every secret in memory.- Patching the pom but not the product: in-house code is upgraded while vendor appliances and shaded jars keep the vulnerable library for months.
- Disabling CSRF for a "REST API" that browsers still call with cookies: the API is then open to cross-site requests.
- Annotations on internal methods:
@PreAuthorizeon a method only ever called from the same bean does nothing. - Legacy middleware left running: an old login or SSO server on outdated Java middleware stays internet-facing with live keystores, as described in the Oracle Cloud claim below.
Lessons from incidents
In March 2025 an actor claimed to have taken around 6 million records, Java keystore (JKS) files and encrypted SSO and LDAP passwords from an Oracle Cloud login server. Researchers reported the server ran Oracle Fusion Middleware 11g and pointed to CVE-2021-35587, a KEV-listed Oracle Access Manager flaw, though that route was not confirmed and Oracle denied a breach of Oracle Cloud Infrastructure, reportedly telling customers that two obsolete servers were accessed. Whatever the final account, the lesson for Java estates is the same: legacy identity middleware with live keystores is part of your attack surface until it is fully decommissioned. Read the Oracle Cloud Breach Claim 2025 analysis.
Framework mapping
| Area | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | OWASP ASVS 5.0 | Other |
|---|---|---|---|---|
| Secure configuration of Spring Security and Actuator | CM-7 Least Functionality, AC-6 Least Privilege | PR.PS-06 | V13.4.2, V13.4.6 | CIS Controls v8.1 Control 16; ISO/IEC 27001:2022 A.8.9, A.8.28 |
| Deserialisation and injection | SA-11 Developer Testing and Evaluation, SA-15 | PR.PS-06 | V1.5.1, V1.5.2, V16.4.1 | CAPEC-586; ATT&CK T1190 |
| Dependency and patch management | SI-2 Flaw Remediation, RA-5, SR-3 | ID.AM-02 | V15.1.1, V15.1.2, V15.2.1 | CIS Controls v8.1 Control 7; ISO/IEC 27001:2022 A.8.8 |
| Secrets and keystores | IA-5 Authenticator Management, SC-12 | PR.AA-01 | V13.3.1 | ATT&CK T1552.001 |
| Detection of exploitation | SI-4 System Monitoring | DE.CM-09 | V16 (logging) | ATT&CK T1505.003 |
Practitioner checklist
- Every
SecurityFilterChainends with a deny or authenticated catch-all, and negative authorisation tests run in CI. @EnableMethodSecurityis on and object-level checks sit on public entry points, not self-invoked methods.- Actuator exposes only what is needed, with
management.endpoints.access.default=none, a dedicated role and no public route to/actuator/**. - CSRF protection stays on for every browser-facing endpoint, and SameSite is set on the session cookie.
- Passwords use
DelegatingPasswordEncoderwith a tuned work factor;NoOpPasswordEncoderappears nowhere. - A
jdk.serialFilterallowlist is in place wherever native deserialisation remains, and XML parsers have external entities disabled. - Each build produces an SBOM, KEV-listed components block release, and vendor Java products are in the inventory.
- Application servers cannot make outbound LDAP or RMI connections.
- No secrets in properties files, images or Git; keystores have unique passwords held in a secrets manager.
- Every Spring, Tomcat and JDK line in use has a known end-of-support date and an upgrade plan.
Standards and references
- Spring: Spring Security Reference, Method Security
- Spring: Spring Security Reference, Cross Site Request Forgery
- Spring: Spring Boot Reference, Actuator Endpoints
- Spring: CVE-2022-22965 Spring Framework RCE via Data Binding on JDK 9+
- Apache Logging Services: Log4j Security Vulnerabilities
- OpenJDK: JEP 290 Filter Incoming Serialization Data
- OWASP: Application Security Verification Standard 5.0
- CISA: Known Exploited Vulnerabilities Catalog
Related NHI Mgmt Group resources: Insecure Deserialisation Guide · OWASP ASVS Guide · Web Application Attacks Guide · Software Supply Chain Security Guide · Secrets Management Guide