On 31 March 2022, the Spring team shipped emergency patches for Spring Framework, closing a remote code execution flaw that the internet had spent 48 hours panicking about under the inevitable nickname Spring4Shell. The vulnerability — CVE-2022-22965, affecting Spring MVC and WebFlux applications running on JDK 9 or later that bind non-primitive request parameters to POJOs — let an attacker abuse Spring’s data binder to reach an object’s ClassLoader through the crafted class.module.classLoader.* property chain, and on the most common deployment shape (WAR on Tomcat) rewrite the AccessLogValve configuration to write a JSP webshell into a web-served directory: file-write to code execution in one request. The panic was understandable — Spring is the backbone of uncountable enterprise Java stacks — but the payload reality was narrower than the headlines: the bug required JDK 9+, a form-binding code pattern many apps didn’t use, and the known turnkey exploit targeted Tomcat specifically, which is why the post-mortem consensus ranked it serious-but-not-Log4Shell. Patches (5.3.18 / 5.2.20) plus rapid guidance from Spring, VMware, and cloud vendors blunted the initial wave; mass scanning began almost immediately, opportunistic attacks followed through April, and CISA added the CVE to its Known Exploited Vulnerabilities catalog — but the widespread enterprise-pocalypse never materialised, and Spring4Shell settled into its real historical role: a masterclass in how a popular framework’s disclosure communications can amplify or dampen a vulnerability’s real risk.
CVE-2022-22965 (“Spring4Shell”, patched 2022-03-31): RCE in Spring Framework’s data binding. Affects Spring MVC/WebFlux apps that bind non-primitive POJO request parameters (form-binding), running on JDK 9+. Root cause: Spring’s
BeanWrapperImpl property resolution was bypassed by Java 9’s module system changes — Class.getModule() now reachable via property path — enabling class.module.classLoader.* chains that earlier JDKs’ Tomcat hardening (the CVE-2010-1622 lineage) had deliberately blocked. Best-known gadget (Apache Tomcat): attacker sets AccessLogValve properties (pattern/suffix/prefix/directory) via bound parameters → Tomcat writes attacker-controlled “log” as a .jsp webshell to a webroot-served path → full RCE. Patched in 5.3.18/5.2.20 (disallow Class getClassLoader/module binding). Preconditions kept blast radius < Log4Shell: JDK 9+, Spring MVC/WebFlux, WAR on servlet container (known exploit chain Tomcat-specific), and a controller binding non-primitive types. Mass scanning started ~2022-04; CISA KEV-listed; real-world victims mostly unpatched internet-facing Tomcat apps (some reports of ransomware/coinminer drop later). Separate but adjacent: CVE-2022-22963 (Spring Cloud Function routing SpEL RCE, also patched late March) — many early reports conflated the two.
What happened
The sequence was speed-run. On 29 March 2022, rumours of a Spring Framework 0-day RCE spread from Chinese-language security chatter after a researcher demonstrated the classloader-binding chain (the report reportedly reached Spring via coordinated disclosure); within a day a PoC was circulating publicly. On 31 March, Spring released 5.3.18 and 5.2.20 with the fix (property binding on Class.getClassLoader() and Class.getModule() disallowed) plus a detailed explainer of the exact preconditions — a document that did more than the patch to calm the panic, because it let operators self-triage. In parallel, a genuinely different bug in Spring Cloud Function (CVE-2022-22963, an unauthenticated SpEL injection in the routing expression) was also going public, and early coverage smeared the two together, inflating the perceived blast radius.
Exploitation followed the usual curve: internet-wide scanning for the Tomcat signature within days, exploit kits and botnet loaders (including Mirai variants and coinminers) by mid-April, and CISA’s KEV listing on 4 April formalising federal patch deadlines. But apps that actually matched the precondition set — JDK 9+, form-binding of non-primitive parameters, WAR-on-Tomcat exposure — were a distinct minority of the Spring ecosystem, and most enterprises had patched inside the two-week window. Damage reports existed (compromised e-commerce and internal apps, webshell persistence) but stayed anecdotal rather than systemic.
The exploit chain
Spring4Shell request path (Tomcat gadget):
attacker HTTP request:
POST /path/formAction
class.module.classLoader.
resources.context.parent.pipeline.
first.pattern=%{c2}i ...
first.suffix=.jsp
first.directory=webapps/ROOT
first.prefix=shell
WHY IT WORKS
JDK 9+ exposes Class.getModule()
as bean property "module"
-> binder escapes the POJO's own
fields into the ClassLoader
-> Spring's disallowed-fields
list only covered JDK 6-8
paths (post-CVE-2010-1622
Tomcat hardening)
EFFECT (Tomcat-specific gadget)
AccessLogValve reconfigured:
prefix/suffix/directory/pattern
next logged request rendered
into webapps/ROOT/shell.jsp
request-header payload (via
%{c2}i / %{c1}i patterns) becomes
executable JSP content
-> webshell -> RCE
PRECONDITIONS (all required for
the known chain)
JDK 9+
Spring MVC or WebFlux
parameter binding to POJO
(non-primitive, non-String)
WAR on servlet container
(turnkey exploit = Tomcat)
PATCH (2022-03-31)
Spring 5.3.18 / 5.2.20
+ earlier mitigations: global
WebDataBinder disallowedFields
(class.module.classLoader etc.)
Impact and numbers
| Metric | Value |
|---|---|
| CVE / patch date | CVE-2022-22965 / 2022-03-31 (5.3.18, 5.2.20) |
| Affected versions | Spring Framework 5.3.0–5.3.17, 5.2.0–5.2.19 (older unsupported also affected) |
| Root cause class | Data binding to ClassLoader via JDK 9+ module property path |
| Turnkey exploit target | Tomcat (AccessLogValve → JSP webshell write) |
| Public PoC circulated | 2022-03-30 |
| CISA KEV listing | 2022-04-04 |
| Exploitation observed | Mass scanning from early April; webshells, coinminers, Mirai-variant loaders |
| Commonly confused sibling | CVE-2022-22963 (Spring Cloud Function SpEL RCE — separate bug, patched 2022-03-29 era) |
Timeline
| Date | Event |
|---|---|
| 2022-03-29 | Rumours of Spring 0-day spread from Chinese security community chatter; researcher (via Code White) had reported the chain responsibly |
| 2022-03-30 | PoC circulating publicly; vendors rush guidance amid Log4Shell-flashback coverage |
| 2022-03-31 | Spring ships 5.3.18/5.2.20 + preconditions explainer; “Spring4Shell” name sticks |
| 2022-04-01→04-04 | Internet-wide Tomcat-signature scanning detected; CISA adds to KEV with federal patch deadline |
| 2022-04 mid | Opportunistic exploitation (webshells, cryptominers, botnet loaders) against unpatched internet-facing apps |
| 2022 later | Consensus assessment: real but bounded damage; lesson absorbed on framework-disclosure communications and JDK-version EOF hygiene |
Why it still matters in 2026
Three reasons it outlived its news cycle. First, the mechanism: Spring4Shell was a regression of a lesson already learned — the classloader-binding family dates to CVE-2010-1622 (Springbean wrapper → Tomcat classloader), and Spring’s blocklist silently rotted when JDK 9’s module system reopened a path around it. That is the canonical argument for treating framework-level input-filtering blocklists as version-coupled attack surface: every runtime underneath (JDK, container, app server) change can resurrect a buried bug, and “we blocked that class of thing in 2014” is not a durable control. Second, the disclosure dynamics: the 29–31 March window featured leaking rumour, a public PoC ahead of coordinated advisory, a name engineered for Log4Shell resonance, and a sibling CVE being conflated in — every ingredient of the modern zero-day infohazard. Spring’s fast, precondition-explicit response became the counter-template vendors now imitate: publish the exact decision tree operators can self-triage against, and name-flames die of starvation. Third, the graveyard of unpatched long-tail: like every KEV entry, Spring4Shell never truly ended — scans for the endpoint signature continue years later, flipping forgotten Tomcat boxes in enterprise DMZs, which is why legacy Java estate and JDK-version awareness remain standing line-items in 2026 audit checklists. And in the bigger 2022 narrative, it was chapter two of “the supply chain’s runtime is the attack surface” — Log4Shell in December, Spring4Shell in March, Confluence’s OGNL in June: the year the industry learned that a single popular library’s parsing code is everyone’s perimeter.
Detection and hardening takeaways
- Patch the framework, not just the app. Spring Framework 5.3.18/5.2.20 was the fix; application-level mitigations (global
disallowedFieldsforclass.module.classLoader) were stopgaps for estates that couldn’t rebuild immediately — audit that no stopgap became permanent. - Treat parameter-binding surfaces as parsing surfaces. Any controller binding request data to rich object graphs is one upstream bug away from type-confusion-style escapes; prefer DTOs with primitives/Strings, strict allowlists, and JDK/container awareness in threat models.
- Harden the gadget, blunt the exploit. The turnkey chain needed Tomcat’s AccessLogValve default reachability and a writable webroot; running apps as non-root with read-only webroots, minimal valve surface, and filesystem integrity monitoring converts “webshell write” into “failed write and alert”.
- Assets + versions = response speed. Operators who could instantly query “which of our apps are JDK 9+, Spring MVC, WAR on Tomcat, internet-facing?” triaged in minutes; SBOMs and runtime inventory are what turned Spring panic into a spreadsheet column.
- Watch for the exploit’s signature, not just the CVE number. Requests containing
class.module.classLoader,pipeline.first, and unusual AccessLogValve pattern tokens are detectable at WAF/ingress years after patching — the long-tail scanners never stopped, and neither should the detection rule.
FAQ
Was Spring4Shell as bad as Log4Shell?
No — narrower on every axis. Log4Shell was a logging-library bug that triggered on essentially any logged string, anywhere, default-on; Spring4Shell required JDK 9+, Spring MVC/WebFlux, a controller binding non-primitive POJO parameters, and (for the turnkey public exploit) a WAR deployment on Tomcat. The affected shape existed in the wild — enough for real compromises — but most Spring estates had patched or didn’t match the pattern, and the doom-scenario worm never came. The shared DNA: both were wildly popular libraries, both were discovered via demo PoCs outpacing advisories, and both lived in the patch long-tail for years afterwards.
What did the exploit actually do on a vulnerable server?
On Tomcat, reconfigured the access-log valve (filename, directory, and log-line pattern) so the next logged request’s attacker-controlled headers got written into a .jsp file inside a served directory — a webshell. From there: arbitrary command execution under the app’s privileges, credential harvesting, pivoting into the container host in bad configurations. Many observed in-the-wild payloads were mundane (cryptominers, Mirai-family botnet loaders), with ransomware and data theft showing up opportunistically rather than as a coordinated campaign.
Is the classloader-binding bug class dead now?
The specific CVE is fully patched, but the class is immortal: any bean-property/OGNL/SpEL-style expression engine that can walk from user input to Class → ClassLoader → container internals is a rediscovery waiting for the next JDK or framework change — exactly what happened between 2010’s CVE-2010-1622 and 2022’s Spring4Shell when JDK 9 modules reopened the door Spring had bolted shut. Defensive posture: keep frameworks current, keep expression-driven binding out of internet-facing endpoints, and maintain regression tests for known-prohibited property paths.
