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-Type
of a request isapplication/x-www-form-urlencoded
, theFormContentFilter
will eventually cause theURLDecoder
to be invoked in order to decode. Now, if the request is invalid (e.g. contains illegal hex characters in my example), anIllegalArgumentException
will 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 theFormContextFilter
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 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).