Search code examples
javaspring-booturldecodingx-www-form-urlencoded

How to handle/suppress Spring Boot DispatcherServlet exceptions related to URLDecoder


I have a fairly simple Spring Boot 3.1 application (with Spring Security) that has needs to have a public endpoint exposed.

At times, I see errors like this one in the logs which are not caused by myself but an unknown third party:

o.a.c.c.C.[.[.[.[dispatcherServlet]: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern
    at java.base/java.net.URLDecoder.decode(URLDecoder.java:230)

I understand this error is caused by invalid PUT, POST, and PATCH requests to the application (see curl to reproduce below). My application does not expose endpoints allowing such methods.

Question: How can/should I handle/surpress this and other URLDecoder related errors that don't apply to my application?

Goal: I don't want these error logs and if there is a security vulnerability, I would like to close it.

I have tried:

1 - Denying requests altogether

As my application only handles GET/POST request, I thought I could simply add something along the lines of auth.requestMatchers(PUT, "/**").denyAll(); to my SecurityFilterChain.

While valid requests are being denied as expected, it appears that the URLDecoder gets to work before the request even hits the SecurityFilterChain. This means that invalid requests such as the below still reach the URLDecoder and cause the above error:

curl -X DELETE -H "Content-Type: application/x-www-form-urlencoded" 'http://localhost:8080/a-non-existing-endpoint' -d "%@"

I assume the reason for this is that "AuthorizationFilter Is Last By Default".

2 - @ControllerAdvice

While not very good for other areas of the application, I have even attempted another radical approach: using @ControllerAdvice for every IllegalArgumentException:

@ControllerAdvice
public class ControllerAdvisor {

  @ExceptionHandler(IllegalArgumentException.class)
  public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) throws IOException {
    log.warn("This is not being called");
    return ResponseEntity.badRequest().body("Some error"));
  }
}

However, this method is not executed when the request is invalid (see curl above).

I would appreciate your thoughts!


Solution

  • A possible solution (but not sure if this is a good way/best way of dealing with this) is to create a custom filter that takes precedence over the FormContentFilter.

    Why? If the Content-Type of a request is application/x-www-form-urlencoded, the FormContentFilter will eventually cause the URLDecoder to be invoked in order to decode. Now, if the request is invalid (e.g. contains illegal hex characters in my example), an IllegalArgumentException will be thrown.

    To be more precise, FormContentFilter.parseIfNecessary(...) may call FormHttpMessageConverter.read(...) which, in turn, calls URLDecoder.decode(...) where the exception is thrown.

    How? Here's a radical example that simply blocks all PATCH/PUT/DELETE:

    @Component
    @Order(-10000)
    public class HttpMethodFilter extends OncePerRequestFilter {
    
      private final List<String> blockedMethods =
          Arrays.asList(HttpMethod.PUT.name(), HttpMethod.PATCH.name(), HttpMethod.DELETE.name());
    
      @Override
      protected void doFilterInternal(
          HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
        var method = request.getMethod().toUpperCase();
        if (blockedMethods.contains(method)) {
          // Do whatever you need to do
          response.setStatus(400);
          return;
        }
        filterChain.doFilter(request, response);
      }
    }
    

    Note that @Order(-10000) is critical here. This is because the order value of the FormContextFilter appears to be -9900, so the value has to be lower than that in order to take precedence.

    Tip: An easy way to see the existing filters/any order in your application is to set a breakpoint somewhere in org.apache.catalina.core.ApplicationFilterChain to see the ApplicationFilterConfig[] filters. This way, you'll be able to see all of them, including their order which goes from Integer.MIN_VALUE (highest precedence) to Integer.MAX_VALUE (lowest precedence).