---
key: "case-study-spring4shell"
title: "Case study: Spring4Shell (CVE-2022-22965)"
url: "https://softwaresecurity.com/domains/appsec/secure-coding/case-study-spring4shell/"
api: "https://softwaresecurity.com/api/v1/articles/case-study-spring4shell.json"
domain: "appsec"
topic: "Application security"
type: "Case study"
identifiers: ["CWE-94","CWE-915","A05:2025","A03:2025","CAPEC-242"]
tags: ["Access control","Dependency management","Vulnerability management","Java"]
stages: ["build","respond"]
roles: ["developer","appsec-engineer","platform-sre"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Case study: Spring4Shell (CVE-2022-22965)

How Spring's data binding let request parameters reach the class loader on JDK 9 and later in 2022, why the denylist workaround leaked, and what it teaches about framework-level mass assignment.

Canonical URL: https://softwaresecurity.com/domains/appsec/secure-coding/case-study-spring4shell/

## In short

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 `disallowedFields` denylist 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

- Date: 2022-03-29 to 2022-04-13
- Category: Vulnerability exploitation
- Organizations: Spring (VMware); Apache Tomcat
- CVE: [CVE-2022-22965](https://nvd.nist.gov/vuln/detail/cve-2022-22965), [CVE-2022-22968](https://nvd.nist.gov/vuln/detail/cve-2022-22968)
- 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

1. 2022-03-29: The flaw is reported to VMware.
2. 2022-03-30: Details leak online while the fix is in progress.
3. 2022-03-31: Spring Framework 5.3.18 and 5.2.20 and CVE-2022-22965 are published.
4. 2022-04-01: Tomcat 10.0.20, 9.0.62 and 8.5.78 close the vector on Tomcat's side.
5. 2022-04-04: CISA adds CVE-2022-22965 to the KEV catalog.
6. 2022-04-13: CVE-2022-22968 shows the disallowedFields workaround is case sensitive.

## Recommended defaults

| 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 |

## Verify it

1. 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.
2. List services deployed as WAR files on standalone Tomcat, Payara or GlassFish and check their container versions too.
3. 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.
4. Add a test that posts an extra parameter (such as a privileged field) and asserts it is not bound.
5. Check that no controller overrides a global binder configuration with its own `@InitBinder` that drops the restriction.

## 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.

Audience: Java and Spring developers, and AppSec and platform teams who triage and roll out framework fixes across many services

Before you start:

- Know how Spring MVC binds request parameters to a `@ModelAttribute` object.
- 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.

Time: About 28 minutes: 10 to read and 18 for the exercise

## A framework feature that let parameters reach the class loader

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.

## Timeline



| 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 |

## Data binding is mass assignment by design

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.

## Why exploitability depended on how the app was deployed

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.

## Why the denylist workaround leaked

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.'

## Find exposed services from their SBOMs

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.

```python
"""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)

```

## Bind only the fields a form declares

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.

```java
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");
    }
}

```

## Which controls would have stopped or limited it?



| 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/ |

## Lessons that still apply



- 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.

## What the public evidence does not establish

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 material

```text
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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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?

Worked answer: 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.

Deliverable: A table of S1 to S5 with exposed (yes, no or not to this exploit), the action, and its deadline.

Scope: 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'.

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [Spring blog: Spring Framework RCE, early announcement](https://spring.io/blog/2022/03/31/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+](https://spring.io/security/cve-2022-22965). 31 March 2022.
- [Spring blog: Spring Framework RCE, mitigation alternative](https://spring.io/blog/2022/04/01/spring-framework-rce-mitigation-alternative). 1 April 2022.
- [Spring security advisory: CVE-2022-22968, Spring Framework data binding rules vulnerability](https://spring.io/security/cve-2022-22968). 13 April 2022.
- [spring-framework commit 002546b: Refine PropertyDescriptor filtering](https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15). 31 March 2022.
- [NVD: CVE-2022-22965](https://nvd.nist.gov/vuln/detail/CVE-2022-22965)
- [CISA Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog). Catalog version 2026.10.08.
- [Spring Framework wiki: Spring Framework versions and support](https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions)