4 ways to fix Spring4Shell Vulnerability in Older versions of Spring

Background

For those of you in the Java world who haven’t heard the news yet, a critical new vulnerability in the Spring framework was recently disclosed. This exploit is considered mature and if unfixed could be used to execute arbitrary code remotely using your favourite Spring based Java software! You’ll find lots of articles online about how you want to upgrade your Spring version to fix the Remote Code Execution (RCE) exploit. Here is a summary of the requirements associated with this expoloit:

  • JDK 9 or higher
  • Apache Tomcat as the Servlet container.
  • Packaged as a traditional WAR (in contrast to a Spring Boot executable jar).
  • spring-webmvc or spring-webflux dependency.
  • Spring Framework versions 5.3.0 to 5.3.17, 5.2.0 to 5.2.19, and older versions.

If you’re able to upgrade to Spring Framework 5.3.18 and 5.2.20, no workarounds are necessary. However, not all of us have the luxury of jumping onto the latest and greatest version of Spring which contains a fix for this issue. So keep reading to see how we solved this issue within our team for products we could not upgrade to the newest version of Spring. Also, pay attention to the version of JDK (9 or higher) as the first requirement, which we’ll come back to later on.

Handling older versions of Spring

Older versions of Spring or Spring Boot applications have some workarounds for fixing this issue. The key aspect of the vulnerability is the `ClassLoader` that can be accessed in unpatched applications. So for older versions, we can use workarounds described in the Spring blog to restrict class loader based access. There are four ways to mitigate this risk

  1. Set disallowedFields on WebDataBinder globally
  2. Using RequestMappingHandlerAdapter to update the WebDataBinder globally
  3. Upgrading to Apache Tomcat 10.0.20, 9.0.62, or 8.5.78
  4. Downgrading to JDK8

While Spring team favours the use of approach #2, this really depends on how your existing/legacy product is writtten. We viewed options #3 and #4 as ones to be used when do not have access to the code, but your mileage might vary!

Using RequestMappingHandlerAdapter to update the WebDataBinder globally

This seems to be the  workaround recommended by the Spring Team. It ensures that `WebDataBinder` is updated only after everything else has been initialised. This makes total sense, given Spring applications (and auto-configuration magic) can do dozens of things in the background as part of initialisation. So here is the snippet of code that works straight out of the box, if you add it to your main class (usually the one annotated with @SpringBootApplication)

  
@Bean
   public WebMvcRegistrations mvcRegistrations() {
       return new WebMvcRegistrations() {
           @Override
           public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
               return new ExtendedRequestMappingHandlerAdapter();
           }
       };
   }


   private static class ExtendedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

       @Override
       protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> methods) {

           return new ServletRequestDataBinderFactory(methods, getWebBindingInitializer()) {

               @Override
               protected ServletRequestDataBinder createBinderInstance(
                   Object target, String name, NativeWebRequest request) throws Exception {

                   ServletRequestDataBinder binder = super.createBinderInstance(target, name, request);
                   String[] fields = binder.getDisallowedFields();
                   List<String> fieldList = new ArrayList<>(fields != null ? Arrays.asList(fields) : Collections.emptyList());
                   fieldList.addAll(Arrays.asList("class.*", "Class.*", "*.class.*", "*.Class.*"));
                   binder.setDisallowedFields(fieldList.toArray(new String[] {}));
                   return binder;
               }
           };
       }
   }

One tiny thing to pay attention to is that the correct classes to import might appear in a dependencies. So here is a list of classes to import for easy reference. Make sure you are importing the correct class from the right package!

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.annotation.InitBinderDataBinderFactory;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;

If in doubt, please consult the Spring article linked to above, which has an example too!

Setting disallowedFields on WebDataBinder globally

In one of our legacy products, short of rewriting a whole lot of classes and re-engieering fragments of this product, we could not use option #1. However, luckily we could use `ControllerAdvice` as the means to set disallowedFields. So here is the snippet of code that we used in a custom ControllerAdvice (based on the Spring Example)

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;

/**
 * A workaround for Sprig4shell  CVE
 * See https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
 * Created by jay on 01/04/2022.
 */
@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);
    }

}

The key here is to ensure that you have specified the `@Order(Ordered.LOWEST_PRECEDENCE)` line, to ensure it is correctly loaded alongside any other ControllerAdvice classes you might have in your application!

Upgrading Tomcat

If you are using an older, usupported version of Spring (you aren’t are you 😜) and you can’t use the above workarounds, there is still hope for you! You can temporarily fix the issue by upgrading to Apache Tomcat 10.0.20, 9.0.62, or 8.5.78!

Downgrading to Java/JDK 8

Right, if you are with me so far, it means you can’t upgrade Spring, you don’t control sourcecode to apply the code workarounds and you can’t even upgrade Tomcat!! 😱 Yes, some of us have been there, but all hope is not lost and you still have a workaround! Just in case you forgot, this expliot needs JDK 9 or above! So if you really can upgrade Spring and you can’t even use the above workarounds, then one mitigation might be to simply downgrade to JDK8.

Thats all folks! Hopefully one of these workarounds helps you find some peace of mind. Please get in touch, if you found other ways of handling the Spring4Shell issue that is not listed here?