Remediation Guides

How to Fix Spring4Shell (CVE-2022-22965) on Spring and Tomcat

26 Eylül 2026 8 min read

Spring4Shell (CVE-2022-22965) is a critical remote code execution flaw in Spring Framework data binding that affects Spring MVC and WebFlux apps running on JDK 9+ and deployed as WAR files on Apache Tomcat. Fix it by upgrading Spring Framework to 5.3.18 or 5.2.20 or later (Spring Boot 2.6.6 or 2.5.12) and redeploying.

The flaw is from 2022, so a hit today usually means an application that has not been rebuilt since. The upgrade itself is a patch release inside the 5.x line; finding every copy of the WAR is the real work.

What the scanner is actually detecting

Finding title Scanner How it is detected
Spring Framework Spring4Shell (CVE-2022-22965) Nessus 159542 (CGI abuses family) Remote check that Tenable marks as exploited by Nessus, so it reflects how the application answered, not a banner. Tenable lists “Test embedded web servers” as a requirement for this plugin
Spring Framework < 5.2.20 / 5.3.x < 5.3.18 Remote Code Execution (Spring4Shell) Tenable WAS 113217 (Component Vulnerability) Component check during a web application scan

Tenable rates the flaw CVSS v3 9.8 and VPR 9.6, and lists exploits in Metasploit and Core Impact. Tenable also ships a separate version-based check that runs only when “Show potential false alarms” (paranoid mode) is enabled, which is a hint that version matching alone produces noise for this CVE.

The mechanism: Spring binds request parameters onto Java objects (for example a @ModelAttribute or plain POJO controller argument). On JDK 9+, a parameter path starting with class.module.classLoader gets around the guard added for CVE-2010-1622, which Tenable describes as a patch bypass. On Tomcat that class loader reaches the AccessLogValve, and public proofs of concept rewrite its directory, prefix, suffix and pattern to write a JSP web shell into the web root. Parameters bound with @RequestBody are not affected.

Real-world risk

When every condition is met, the attack needs no credentials and no user interaction, and commands run as the Tomcat service account. CISA added CVE-2022-22965 to the Known Exploited Vulnerabilities catalog on April 4, 2022 (see how the CISA KEV catalog works). Microsoft reported a low volume of exploit attempts that closely matched the basic web shell proof of concept.

The conditions are narrower than the headlines suggested. Spring’s advisory lists all of these as required for the known exploit:

  • JDK 9 or higher
  • Apache Tomcat as the servlet container
  • Packaging as a traditional WAR (a Spring Boot executable jar, the default, is not vulnerable to this exploit)
  • spring-webmvc or spring-webflux
  • Spring Framework 5.3.0 to 5.3.17, 5.2.19 or earlier, or older unsupported versions

Spring also warns that the underlying issue is more general and other exploit paths may exist, so an executable jar on JDK 17 should still be upgraded, just with less urgency than an internet-facing WAR.

How to confirm it on the host

Check the Tomcat and Java versions

# Linux: prints "Server version" and "JVM Version"
"$CATALINA_HOME/bin/version.sh"

# The java binary the running Tomcat actually uses
ps -eo pid,args | grep '[o]rg.apache.catalina.startup.Bootstrap'

version.sh reports the JVM from your shell environment and setenv.sh; a systemd unit can set a different JAVA_HOME, so trust the running process. On Windows run %CATALINA_HOME%binversion.bat, and for a Tomcat service check the Java tab of the service configuration utility (for example tomcat9w.exe), because the service can use a different JVM than your shell.

Find the Spring version inside deployed WAR files

# Spring jars in exploded webapps
sudo find / -path '*WEB-INF/lib/spring-*.jar' 2>/dev/null | grep -E 'spring-(beans|webmvc|webflux)-'

# Spring jars packed inside WAR files
sudo find / -name '*.war' 2>/dev/null | while read -r f; do
  unzip -l "$f" 2>/dev/null | grep -E 'spring-(beans|webmvc|webflux)-[0-9].*.jar' | sed "s|^|$f: |"
done

# Confirm the real version from the jar manifest
unzip -p spring-beans-5.3.17.jar META-INF/MANIFEST.MF | grep Implementation-Version
# From source
mvn dependency:tree -Dincludes=org.springframework
./gradlew dependencyInsight --dependency spring-beans --configuration runtimeClasspath

Build-time dependency scanning (SCA) finds vulnerable Spring versions that a web scan never reaches; how SAST, DAST and SCA differ explains why you want both views for a library flaw like this.

Optional remote probe

Microsoft published a quick indicator: a request that tries to bind a harmless value through the class loader path.

curl -i 'http://host:port/path?class.module.classLoader.URLs%5B0%5D=0'

Point it at an endpoint that binds request parameters. Microsoft says an HTTP 400 response means the host should be treated as vulnerable, but also that any system meeting the conditions above should be treated as vulnerable whatever the probe returns.

How to fix Spring4Shell

Spring Boot applications deployed as WAR

Upgrade Spring Boot to 2.6.6 or 2.5.12 or later. Spring Boot 2.6.6 manages Spring Framework 5.3.18.

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.6.6</version> <!-- or any later release -->
</parent>

If you inherit from spring-boot-starter-parent but cannot move Boot yet, the Spring Boot docs allow overriding a managed version with a property. The Spring Framework property is spring-framework.version:

<properties>
  <spring-framework.version>5.3.18</spring-framework.version>
</properties>

Treat this as a bridge and test it, since an older Boot line was built against an older Framework. If you import spring-boot-dependencies instead of using the parent, the override entry must come before that import in dependencyManagement.

Plain Spring MVC or WebFlux applications

Set every org.springframework artifact to 5.3.18 or later (5.2.20 or later on the 5.2 line), ideally through the spring-framework-bom so no module is left behind. Then rerun the dependency tree check and confirm no older spring-beans arrives transitively.

Plan beyond the minimum. Spring’s version page states that open source support for 5.3.x ended in August 2024 and that 7.0.x is the current production line. Moving to Spring 6 or 7 means JDK 17 and the Jakarta EE namespace, so it is a project, not a patch.

Redeploy cleanly

sudo systemctl stop tomcat
sudo cp /opt/tomcat/webapps/myapp.war /root/myapp.war.pre-upgrade
sudo rm -rf /opt/tomcat/webapps/myapp /opt/tomcat/work/Catalina/localhost/myapp
sudo cp target/myapp.war /opt/tomcat/webapps/
sudo systemctl start tomcat

Service names and paths vary. Removing the exploded folder keeps Tomcat from serving the old jars.

Vendor products

NVD lists affected products from Oracle, Cisco, Siemens and Veritas among others. For those, apply the vendor’s patch rather than swapping jars inside the product.

Stopgaps when the Spring upgrade has to wait

  • Upgrade Tomcat. Spring states that Tomcat 10.0.20, 9.0.62 or 8.5.78 provides adequate protection, as a tactical measure. Tomcat 8.5 and 10.0 are now end of life, and Spring 5 applications use the javax namespace, so in practice this means a current 9.0.x release.
  • Run on Java 8. Spring calls this viable if you can upgrade neither Spring nor Tomcat. It only works for code built for Java 8.
  • Disallowed fields. Spring’s published global binder rule:
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
    @InitBinder
    public void setAllowedFields(WebDataBinder dataBinder) {
        String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
        dataBinder.setDisallowedFields(denylist);
    }
}

Spring notes this can leave loopholes when a controller sets disallowedFields in its own @InitBinder, which overrides the global setting. For a stronger version it recommends extending RequestMappingHandlerAdapter through a WebMvcRegistrations or WebFluxRegistrations bean.

Check exposed hosts for compromise

  • Look for JSP files in the web roots that are not in your build artifact. Tenable’s WAS backdoor check looks for tomcatwar.jsp and Microsoft’s write-up describes a shell.jsp, but names are trivial to change.
  • Search the logs: sudo grep -rl ‘class.module.classLoader’ “$CATALINA_BASE/logs”. Default access logs do not record POST bodies, so no hit does not prove nothing happened.
  • Check for access log files with unexpected names or locations, since the exploit repoints the valve.

How to verify the fix and rescan

  1. Rerun the jar search and manifest check against the deployed webapp, not only the build output.
  2. Confirm the application starts and its forms still bind correctly.
  3. Rescan with a Nessus policy that includes 159542 and has “Test embedded web servers” enabled.
  4. Rerun the Tenable WAS scan so 113217 closes too. If you only applied the Tomcat or Java stopgap, expect version-based checks to keep flagging Spring until it is upgraded.

What can break and how to roll back

Change What may break
Spring 5.3.x to 5.3.18+ (or 5.2.x to 5.2.20+) Patch release on the same line; regressions are rare, but run your binding and form tests
Overriding spring-framework.version on an old Boot Boot modules compiled against an older Framework may misbehave at startup
Disallowed fields rule Any form field whose path contains a class segment silently stops binding
Java 8 downgrade Code or libraries compiled for Java 9+ fail to load

Rollback is the saved WAR: stop Tomcat, restore it, clear the exploded and work folders, start again. Rolling back reopens the CVE, so keep a stopgap in place while you fix the new build.

Common false positive reasons

  • Wrong asset. The IP is a reverse proxy or load balancer and the finding belongs to a backend.
  • Stale results. The scan predates the redeploy, or an old exploded folder was still being served.
  • Version matching without the conditions. Version-based checks flag Spring in executable jars or on JDK 8, where the known exploit does not work. Upgrade anyway, but the priority is lower.
  • Wrong CVE. CVE-2022-22963 is a separate Spring Cloud Function flaw that Tenable calls completely unrelated; the two were confused because Spring4Shell had no CVE at first.

FAQ

We run Spring Boot as an executable jar. Are we affected?

Not by the known exploit, which needs a WAR on standalone Tomcat. Spring still recommends upgrading because other exploit paths may exist.

Does upgrading Tomcat close the finding?

It blocks the Tomcat exploit path, but Spring is still vulnerable and version-based checks will keep reporting it. Treat it as a stopgap.

Is Spring4Shell exploited in the wild?

Yes. It is in the CISA KEV catalog, and public exploits exist in Metasploit and Core Impact.

Which Spring version should we target?

5.3.18 or 5.2.20 closes this CVE, but both 5.x lines are out of open source support. Plan a move to a supported generation.

Tracking this finding across many hosts

The same WAR is often deployed on several servers, and each copy has to be rebuilt. SITEY, a self-hosted vulnerability management platform, imports Nessus results from an uploaded .nessus export, adds CISA KEV and FIRST EPSS context, and can re-test an individual Nessus finding such as 159542 after the redeploy. Duplicates are merged per scanner, so a host reported by Nessus and by a second scanner still appears as two findings.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing