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!
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-Typeof a request isapplication/x-www-form-urlencoded, theFormContentFilterwill eventually cause theURLDecoderto be invoked in order to decode. Now, if the request is invalid (e.g. contains illegal hex characters in my example), anIllegalArgumentExceptionwill be thrown.To be more precise,
FormContentFilter.parseIfNecessary(...)may callFormHttpMessageConverter.read(...)which, in turn, callsURLDecoder.decode(...)where the exception is thrown.How? Here's a radical example that simply blocks all
PATCH/PUT/DELETE:Note that
@Order(-10000)is critical here. This is because the order value of theFormContextFilterappears 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.ApplicationFilterChainto see theApplicationFilterConfig[] filters. This way, you'll be able to see all of them, including their order which goes fromInteger.MIN_VALUE(highest precedence) toInteger.MAX_VALUE(lowest precedence).