Spring4Shell is the community's name for CVE-2022-22965, a remote code execution flaw in Spring Framework disclosed on 31 March 2022. Spring's data binding, which copies request parameters onto an object's properties, could reach the application's class loader on JDK 9 and later; the reported exploit needed the application packaged as a WAR on Apache Tomcat. It was fixed in Spring Framework 5.3.18 and 5.2.20, after details leaked online before the release. The lesson is mass assignment at framework level: bind only the fields a form declares, and upgrade frameworks on a supported line rather than relying on denylist workarounds.
- Data binding treats request parameter names as property paths, so a parameter can reach nested properties the developer never meant to expose.
- The reported exploit needed JDK 9+, a WAR deployed on a standalone Servlet container such as Tomcat, and spring-webmvc or spring-webflux; Spring Boot executable jars were not vulnerable to it, but Spring warned the flaw was more general.
- Fixed in Spring Framework 5.3.18 and 5.2.20 (Spring Boot 2.6.6 and 2.5.12); Tomcat 10.0.20, 9.0.62 and 8.5.78 closed the vector on Tomcat's side.
- The
disallowedFieldsdenylist workaround had loopholes; a follow-up, CVE-2022-22968, showed its patterns were case sensitive. - In October 2026 only Spring Framework 7.0.x gets open-source fixes: 5.3.x ended in August 2024 and 6.2.x in June 2026.
- The specific exploit path and payloads come from third-party write-ups; Spring's own posts describe ClassLoader access in general terms.
Incident at a glance
- Dates
- to
- Category
- Vulnerability exploitation
- Organizations
- Spring (VMware)
- Apache Tomcat
- Impact
- Spring MVC and WebFlux applications on JDK 9+ were open to remote code execution through data binding; the reported exploit needed a WAR on Tomcat. Details leaked before the fix, and CISA listed it as exploited.
- How it was found
- Reported privately to VMware by codeplutos and meizjm3i of AntGroup FG Security Lab; details then leaked online before the fix.
Timeline
- The flaw is reported to VMware.
- Details leak online while the fix is in progress.
- Spring Framework 5.3.18 and 5.2.20 and CVE-2022-22965 are published.
- Tomcat 10.0.20, 9.0.62 and 8.5.78 close the vector on Tomcat's side.
- CISA adds CVE-2022-22965 to the KEV catalog.
- CVE-2022-22968 shows the disallowedFields workaround is case sensitive.
| Setting | Value | Note | Source |
|---|---|---|---|
| CVE-2022-22965 | CVSS 3.1 9.8; CWE-94 | In CISA's KEV catalog since 4 April 2022. | NVD; CISA KEV |
| Affected | Spring Framework 5.3.0 to 5.3.17, 5.2.0 to 5.2.19 and older | With spring-webmvc or spring-webflux on JDK 9 or later. | Spring advisory |
| Fixed | Spring Framework 5.3.18 and 5.2.20; Spring Boot 2.6.6 and 2.5.12 | Released 31 March 2022. | Spring advisory and blog |
| Tomcat-side fix | Apache Tomcat 10.0.20, 9.0.62 and 8.5.78 | A tactical measure; Spring still advised upgrading the framework. | Spring, Mitigation Alternative |
| Follow-up | CVE-2022-22968: disallowedFields patterns are case sensitive | Fixed in 5.3.19 and 5.2.21, 13 April 2022. | Spring advisory CVE-2022-22968 |
| Supported today | Spring Framework 7.0.x (open source) | 5.3.x open-source support ended in August 2024 and 6.2.x in June 2026; commercial support exists for both. | Spring Framework versions wiki |
- Generate an SBOM for every Java service and confirm no spring-beans, spring-webmvc or spring-webflux below 5.3.18 or 5.2.20 remains, then confirm each service is on a Spring Framework line that still gets fixes.
- List services deployed as WAR files on standalone Tomcat, Payara or GlassFish and check their container versions too.
- For each controller that binds request parameters to an object, confirm the object is a dedicated request type with only the fields the form needs, or that the binder has an allowlist.
- Add a test that posts an extra parameter (such as a privileged field) and asserts it is not bound.
- Check that no controller overrides a global binder configuration with its own
@InitBinderthat drops the restriction.
- Applies to
- Spring Framework (spring-webmvc, spring-webflux) Vulnerable: 5.3.0 to 5.3.17, 5.2.0 to 5.2.19 and older on JDK 9+; fixed: 5.3.18 and 5.2.20; supported in October 2026: 7.0.x
- Apache Tomcat as the Servlet container 10.0.19, 9.0.61, 8.5.77 and earlier known vulnerable; 10.0.20, 9.0.62 and 8.5.78 close the vector
Java applications built on Spring MVC or Spring WebFlux that bind request parameters to objects, and teams that respond to vulnerabilities in widely used frameworks.
- Part of
- Workflow: Respond to a vulnerability. Collection: case studies
Tested fixes: the vulnerable code, the fix, a plausible fix that still fails, and the regression tests that tell them apart.
Learning goals and prerequisites
What you will learn
- Explain how Spring's data binding let request parameters reach class-loader properties on JDK 9 and later.
- Decide whether a given deployment matched the reported exploit's conditions, and why that is not the same as safe.
- Replace denylist binding workarounds with allowlists or dedicated request types, and test them.
- Plan framework upgrades so a critical fix lands on a supported line.
Before you start
Java and Spring developers, and AppSec and platform teams who triage and roll out framework fixes across many services
- Know how Spring MVC binds request parameters to a
@ModelAttributeobject. - Know what mass assignment is.
- For the code: Python 3 for the SBOM check; JDK 17 or later and Spring Framework 7 for the binder example.
Refresh first: Authorize functions and fields as well as objects
Go to the guided practiceHow a request parameter reached the class loader
- Bind the requestA controller takes a form object, and Spring copies each request parameter onto the property path its name describes, such as
address.city. - Walk past the formEvery Java object has a
getClass()method, so a parameter name starting withclass.walks from the form into theClassobject and on to the objects it exposes. - Miss the new pathSpring filtered
Class.classLoaderby name. On JDK 9 and later,Classhas other getters, and the filter did not stop a path that reached a ClassLoader another way. - Change the containerOn Tomcat, the class loader of a WAR application exposes properties that change how the server behaves, which third-party write-ups show being used to write a file and run code.
On 31 March 2022 the Spring team announced 'an RCE vulnerability in the Spring Framework that was leaked out ahead of CVE publication.' It had been reported to VMware two days earlier by codeplutos and meizjm3i of AntGroup FG Security Lab; while the team worked on emergency releases, 'details were leaked in full detail online'. Spring Framework 5.3.18 and 5.2.20 shipped the same day as CVE-2022-22965, rated critical. The community called it Spring4Shell, echoing Log4Shell from December 2021; Spring's own posts do not use the name.
The flaw was in data binding, the feature that fills a Java object from request parameters. Spring's advisory: 'A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment.' A Spring Boot executable jar, the default, 'is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.'
That combination, a critical bug in one of the most used Java frameworks whose exploitability depended on deployment details, made it a hard triage problem. CISA added it to the Known Exploited Vulnerabilities catalog on 4 April 2022.
| When | Event | Source |
|---|---|---|
| 2022-03-29 | The issue is reported to VMware late on Tuesday evening (GMT) by codeplutos and meizjm3i of AntGroup FG. | Spring early announcement |
| 2022-03-30 | While Spring works on a fix, details are leaked in full online. | Spring early announcement |
| 2022-03-31 | Spring publishes an early announcement, then Spring Framework 5.3.18 and 5.2.20, Spring Boot 2.6.6 and 2.5.12, and CVE-2022-22965. The fix commit refines property filtering in CachedIntrospectionResults. | Spring blog and advisory; spring-framework commit 002546b |
| 2022-04-01 | Spring publishes a mitigation alternative: Apache Tomcat 10.0.20, 9.0.62 and 8.5.78 close the attack vector on Tomcat's side. | Spring, Mitigation Alternative |
| 2022-04-04 | CISA adds CVE-2022-22965 to the KEV catalog. | CISA KEV |
| 2022-04-08 | Spring notes an additional attack vector for GlassFish and Payara reported by Snyk. | Spring early announcement, updates |
| 2022-04-13 | CVE-2022-22968: the disallowedFields patterns used in the suggested workaround are case sensitive; fixed in 5.3.19 and 5.2.21. | Spring advisory CVE-2022-22968 |
| 2024-08 | Open-source support for Spring Framework 5.3.x ends. | Spring Framework versions wiki |
Spring's announcement explains the mechanism in one line: 'The issue relates to data binding used to populate an object from request parameters (either query parameters or form data).' Binding applies to controller parameters 'annotated with @ModelAttribute or optionally without it, and without any other Spring Web annotation.' A parameter named address.city sets getAddress().setCity(...): the name is a path through the object's properties. That is convenient, and it means the client chooses which properties to set.
Every Java object has a getClass() method, which the bean machinery sees as a property called class, so a path that starts with class. leaves the form object and walks into the Class object. Spring already blocked the dangerous step: before the fix, CachedIntrospectionResults ignored the classLoader and protectionDomain properties of Class by name. The fix commit, 'Refine PropertyDescriptor filtering', replaced that with two broader rules: on Class, allow only name and its variants; anywhere, ignore every property whose type is a ClassLoader or ProtectionDomain. The JDK 9 condition fits that change: JDK 9 gave Class new getters, including getModule(), and Module has its own getClassLoader(). Third-party exploit write-ups describe exactly that route, through class.module.classLoader; Spring's own posts say only that 'The vulnerability involves ClassLoader access and depends on the actual Servlet Container in use.'
This is the same weakness as mass assignment (CWE-915) in application code, one level down: the framework, not the developer, decided which properties were reachable. The application-level guide to mass assignment (/domains/appsec/authorization/function-property-authorization/) and the Spring mass-assignment fix card (/fix/mass-assignment/java-spring/) apply the same remedy, an explicit allowlist of bindable fields.
Spring listed the requirements for the reported scenario: JDK 9 or higher; packaged as a traditional WAR and deployed on a standalone Servlet container; spring-webmvc or spring-webflux; and an affected Spring Framework version. 'Typical Spring Boot deployments using an embedded Servlet container or reactive web server are not impacted.' Tomcat 10.0.19, 9.0.61, 8.5.77 and earlier were known to be vulnerable, as were Payara and GlassFish, and 'Other Servlet containers may also be vulnerable.'
That made triage harder, not easier. A team with hundreds of services had to know, for each one, the JDK, the packaging, the container and the binding style, and the answer 'Boot jar, so not vulnerable to this exploit' came with Spring's caveat that other exploitation paths might exist. With an SBOM and deployment metadata per service, that is a query; without them, it means reading build files and deployment scripts service by service. The dependency response guide (/domains/appsec/vulnerability-response/dependency-vulnerability-response/) covers the workflow, and the Log4Shell case study (/domains/appsec/vulnerability-response/case-study-log4shell/) the same race four months earlier.
For teams that could not upgrade at once, Spring offered three workarounds: upgrade Tomcat, downgrade to Java 8, or set disallowedFields on every WebDataBinder globally with the patterns class.*, Class.*, *.class.* and *.Class.*. Spring itself warned that the global setting 'may leave some loopholes, in particular if a controller sets disallowedFields locally through its own @InitBinder method, which overrides the global setting', and gave a more fail-safe variant that extends RequestMappingHandlerAdapter.
Two weeks later CVE-2022-22968 showed another gap: the patterns 'are case sensitive', so a field is protected only if both the upper- and lower-case first letter are listed, including in nested paths. A denylist has to anticipate every spelling of every dangerous path; an allowlist only has to list the fields the form needs. The Tomcat upgrade was likewise described as 'a tactical solution, and the main goal should be to upgrade to a currently supported Spring Framework version as soon as possible.'
The script reads a CycloneDX JSON SBOM and flags spring-beans, spring-webmvc and spring-webflux below the fixed versions, Tomcat below the releases that closed the vector, and any Spring Framework line that no longer gets open-source fixes. Pass --war for services packaged as WAR files, which matched the reported exploit's conditions. It ran with Python 3.14 on two sample SBOMs: one with Spring 5.3.17 and Tomcat 9.0.60, which it flags, and one with Spring 7.0.8, which it passes. Update OSS_SUPPORTED from Spring's versions page when you use it.
"""Check a CycloneDX JSON SBOM for CVE-2022-22965 (Spring4Shell) exposure.
Flags spring-beans, spring-webmvc and spring-webflux below 5.2.20 / 5.3.18,
Tomcat below 8.5.78 / 9.0.62 / 10.0.20, and Spring Framework lines that no
longer get open-source fixes. Usage: spring4shell_sbom.py sbom.json [--war]
"""
import json
import re
import sys
SPRING = {"spring-beans", "spring-webmvc", "spring-webflux"}
SPRING_FIXED = {(5, 2): (5, 2, 20), (5, 3): (5, 3, 18)}
TOMCAT_FIXED = {(8, 5): (8, 5, 78), (9, 0): (9, 0, 62), (10, 0): (10, 0, 20)}
OSS_SUPPORTED = {(7, 0)} # check the Spring Framework versions wiki when you run this
def parse(version):
return tuple(int(n) for n in re.findall(r"\d+", version)[:3])
def findings(sbom, war):
for c in sbom.get("components", []):
group, name, v = c.get("group", ""), c.get("name", ""), parse(c.get("version", "0"))
if group == "org.springframework" and name in SPRING:
fixed = SPRING_FIXED.get(v[:2])
if v[:2] < (5, 2) or (fixed and v < fixed):
yield f"{name} {c['version']}: vulnerable to CVE-2022-22965" + (
" (packaged as a WAR, as the reported exploit requires)" if war else "")
if v[:2] not in OSS_SUPPORTED:
yield f"{name} {c['version']}: line {v[0]}.{v[1]} gets no open-source fixes; upgrade"
if group.startswith("org.apache.tomcat") and name in ("tomcat-embed-core", "tomcat-catalina"):
fixed = TOMCAT_FIXED.get(v[:2])
if fixed and v < fixed:
yield f"{name} {c['version']}: below the Tomcat release that closes the vector"
if __name__ == "__main__":
with open(sys.argv[1], encoding="utf-8") as f:
found = list(findings(json.load(f), "--war" in sys.argv))
print("\n".join(found) or "no Spring4Shell exposure found")
sys.exit(1 if found else 0)Upgrading is the fix. As defense in depth, give each form a binder allowlist, or bind to a dedicated request type that has only the fields the client may set. The advice below applies setAllowedFields to one named model attribute. A JUnit test bound email, admin and class.name to the form with this configuration: only email was set, and the binding result listed admin and class.name as suppressed fields. It compiled and passed against Spring Framework 7.0.8 on JDK 21. Name the model attribute in @InitBinder so the allowlist applies only to the form it was written for.
package example;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
// Defense in depth, not a substitute for a fixed Spring Framework:
// bind only the fields each form declares, so request parameters can never
// reach other properties, nested objects or framework internals.
@ControllerAdvice
public class BindingAllowlist {
@InitBinder("signupForm")
public void signupFields(WebDataBinder binder) {
binder.setAllowedFields("email", "name");
}
}| Control | Who | SSDF practice or standard | Guide |
|---|---|---|---|
| Restrict binding by type and path in the framework; review every reachable property when the platform adds APIs (JDK 9 here) | Framework | SSDF PW.1.1, PW.7.2 | /domains/appsec/secure-coding/framework-security-patterns/ |
| Look for the whole class after a fix, as CVE-2022-22968 did for the workaround | Framework | SSDF RV.3.3 | /domains/appsec/vulnerability-response/coordinated-disclosure/ |
| Allowlist bindable fields, or bind to dedicated request types | Application team | SSDF PW.5.1; CWE-915 | /domains/appsec/authorization/function-property-authorization/ |
| An SBOM per service plus deployment metadata (JDK, packaging, container) | Application team | SSDF PW.4.4, PS.3.2 | /domains/supply-chain/sbom/generate-release-inventory/ |
| Stay on a framework line that still gets fixes, and rehearse emergency upgrades | Application team | SSDF PW.4.1, RV.2.2 | /domains/appsec/vulnerability-response/dependency-vulnerability-response/ |
| Prioritize by KEV status and exposure, not CVSS alone | Application team | SSDF RV.2.1; CISA KEV | /frameworks/vulnerability-prioritization/ |
- Binding is mass assignment by design. Decide which fields a request may set, per form, and enforce it with an allowlist or a dedicated type.
- A denylist of dangerous paths is a race against spellings; CVE-2022-22968 lost it within two weeks.
- Exploitability that depends on deployment details makes triage slower unless you already know each service's JDK, packaging and container.
- Patch the framework, not just the container: the Tomcat fix closed one route, and Spring warned of others.
- Being on a supported line is the precondition for a quick fix. In October 2026 that means Spring Framework 7.0.x for open-source support.
Spring's posts do not describe the exploit path or payloads; the route through class.module.classLoader and the use of Tomcat's logging configuration come from third-party exploit write-ups, consistent with the fix commit but not confirmed by Spring. The public record does not establish how widely CVE-2022-22965 was exploited or by whom, beyond CISA's KEV listing of known exploitation. Whether deployments outside Tomcat, Payara and GlassFish, or Spring Boot executable jars, could be exploited in practice is not established either way; Spring's statement that 'there may be other ways to exploit it' is a caution, not a finding.
Guided practice
It is 1 April 2022. You own five Java services. Decide each one's exposure and action. Treat the facts as true.
Supplied scenario material
S1: Spring Framework 5.3.16, packaged as a WAR on standalone Tomcat 9.0.58, JDK 11; controllers bind forms with @ModelAttribute.
S2: Spring Boot 2.6.4 (Spring Framework 5.3.16), executable jar with embedded Tomcat, JDK 17.
S3: Spring Framework 5.3.16, WAR on Tomcat 9.0.58, JDK 8.
S4: Spring Framework 5.3.18, WAR on Tomcat 9.0.58, JDK 11; one controller has its own @InitBinder that calls setDisallowedFields("internalId").
S5: Spring Framework 4.3.30 (end of life), WAR on Tomcat 8.5.70, JDK 11; the team says upgrading Spring will take a quarter.1. Decide S1
Is S1 exposed, and what do you do?
Expected result and reasoning
Yes: it meets every condition of the reported exploit (affected version, JDK 9+, WAR on Tomcat, data binding). Upgrade to 5.3.18 today; if that takes hours, upgrade Tomcat to 9.0.62 first and apply an allowlist or the global workaround in the meantime.
2. Decide S2
S2 is a Boot jar. Can you leave it?
Expected result and reasoning
It is not vulnerable to the reported exploit, but it runs an affected version and Spring warned of other possible paths. Upgrade to Spring Boot 2.6.6 on the normal fast track, after S1 and S5.
3. Decide S3
S3 runs on JDK 8. Is it safe?
Expected result and reasoning
Not vulnerable to this issue as described, since it needs JDK 9+. Still upgrade Spring, and note that a JDK upgrade later would expose it if Spring were not upgraded first.
4. Decide S4
S4 is on the fixed version. Anything left?
Expected result and reasoning
CVE-2022-22965 is fixed. The local setDisallowedFields shows the pattern Spring warned about, a controller overriding global binder settings; replace denylists with allowlists, and plan 5.3.19 for CVE-2022-22968.
5. Decide S5
What do you do with S5?
Expected result and reasoning
It is on an end-of-life Spring line with no fix and meets the exploit conditions. Upgrade Tomcat to 8.5.78 now, add binder allowlists, restrict exposure, and treat the Spring migration as urgent rather than a quarter-long project.
The fixture uses the conditions Spring published for the reported exploit. Spring cautioned that other exploitation paths may exist, so 'not vulnerable to this exploit' is not the same as 'safe to leave unpatched'.
Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.
No draft saved on this device yet.
Sources & further reading
The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.
- Spring blog: Spring Framework RCE, early announcement. 31 March 2022, updated through 13 April 2022.
- Spring security advisory: CVE-2022-22965, Spring Framework RCE via data binding on JDK 9+. 31 March 2022.
- Spring blog: Spring Framework RCE, mitigation alternative. 1 April 2022.
- Spring security advisory: CVE-2022-22968, Spring Framework data binding rules vulnerability. 13 April 2022.
- spring-framework commit 002546b: Refine PropertyDescriptor filtering. 31 March 2022.
- NVD: CVE-2022-22965
- CISA Known Exploited Vulnerabilities catalog. Catalog version 2026.10.08.
- Spring Framework wiki: Spring Framework versions and support
Reference information
- SDLC phase
- DevelopmentMaintenance
- CAPEC attack patterns