I have a REST API with spring-boot 2.5.5 with spring-security 5.5.2.
On many API endpoints I use @PathVariable for resources identifiers.
But when some special (but valid in my domain context) characters are passed as PathVariables, the request fails.
The three failing special characters are : slash (/), semicolon (;) and percent (%).
Let's take a very minimal example with the following :
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class Controller {
    @GetMapping("echo/{value}")
    public ResponseEntity<String> echo(@PathVariable("value") String value) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(value);
    }
}
For value "hello/there", I send GET /api/v1/echo/hello%2Fthere and I receive :
<!doctype html><html lang="en"><head><title>HTTP Status 400 – Bad Request</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 400 – Bad Request</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Message</b> Invalid URI: noSlash</p><p><b>Description</b> The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).</p><hr class="line" /><h3>Apache Tomcat/9.0.53</h3></body></html>
For value "hello;there", I send GET /api/v1/echo/hello%3Bthere and I receive :
{
  "stackTrace": [
    {
      "classLoaderName": "app",
      "methodName": "handleAccessDeniedException",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 194,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "handleSpringSecurityException",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 173,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 142,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ExceptionTranslationFilter.java",
      "lineNumber": 115,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.ExceptionTranslationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SessionManagementFilter.java",
      "lineNumber": 126,
      "nativeMethod": false,
      "className": "org.springframework.security.web.session.SessionManagementFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SessionManagementFilter.java",
      "lineNumber": 81,
      "nativeMethod": false,
      "className": "org.springframework.security.web.session.SessionManagementFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "AnonymousAuthenticationFilter.java",
      "lineNumber": 105,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.AnonymousAuthenticationFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextHolderAwareRequestFilter.java",
      "lineNumber": 149,
      "nativeMethod": false,
      "className": "org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "RequestCacheAwareFilter.java",
      "lineNumber": 63,
      "nativeMethod": false,
      "className": "org.springframework.security.web.savedrequest.RequestCacheAwareFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "LogoutFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.logout.LogoutFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "LogoutFilter.java",
      "lineNumber": 89,
      "nativeMethod": false,
      "className": "org.springframework.security.web.authentication.logout.LogoutFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextPersistenceFilter.java",
      "lineNumber": 110,
      "nativeMethod": false,
      "className": "org.springframework.security.web.context.SecurityContextPersistenceFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "SecurityContextPersistenceFilter.java",
      "lineNumber": 80,
      "nativeMethod": false,
      "className": "org.springframework.security.web.context.SecurityContextPersistenceFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ChannelProcessingFilter.java",
      "lineNumber": 133,
      "nativeMethod": false,
      "className": "org.springframework.security.web.access.channel.ChannelProcessingFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 336,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy$VirtualFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilterInternal",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 211,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "FilterChainProxy.java",
      "lineNumber": 183,
      "nativeMethod": false,
      "className": "org.springframework.security.web.FilterChainProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "invokeDelegate",
      "fileName": "DelegatingFilterProxy.java",
      "lineNumber": 358,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.DelegatingFilterProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "DelegatingFilterProxy.java",
      "lineNumber": 271,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.DelegatingFilterProxy"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilterInternal",
      "fileName": "RequestContextFilter.java",
      "lineNumber": 100,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.RequestContextFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 119,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "OncePerRequestFilter.java",
      "lineNumber": 103,
      "nativeMethod": false,
      "className": "org.springframework.web.filter.OncePerRequestFilter"
    },
    {
      "classLoaderName": "app",
      "methodName": "internalDoFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 189,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "doFilter",
      "fileName": "ApplicationFilterChain.java",
      "lineNumber": 162,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationFilterChain"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 711,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "processRequest",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 461,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "doForward",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 385,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "forward",
      "fileName": "ApplicationDispatcher.java",
      "lineNumber": 313,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.ApplicationDispatcher"
    },
    {
      "classLoaderName": "app",
      "methodName": "custom",
      "fileName": "StandardHostValve.java",
      "lineNumber": 403,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "status",
      "fileName": "StandardHostValve.java",
      "lineNumber": 249,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "throwable",
      "fileName": "StandardHostValve.java",
      "lineNumber": 344,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "StandardHostValve.java",
      "lineNumber": 169,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardHostValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "ErrorReportValve.java",
      "lineNumber": 92,
      "nativeMethod": false,
      "className": "org.apache.catalina.valves.ErrorReportValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "invoke",
      "fileName": "StandardEngineValve.java",
      "lineNumber": 78,
      "nativeMethod": false,
      "className": "org.apache.catalina.core.StandardEngineValve"
    },
    {
      "classLoaderName": "app",
      "methodName": "service",
      "fileName": "CoyoteAdapter.java",
      "lineNumber": 357,
      "nativeMethod": false,
      "className": "org.apache.catalina.connector.CoyoteAdapter"
    },
    {
      "classLoaderName": "app",
      "methodName": "service",
      "fileName": "Http11Processor.java",
      "lineNumber": 382,
      "nativeMethod": false,
      "className": "org.apache.coyote.http11.Http11Processor"
    },
    {
      "classLoaderName": "app",
      "methodName": "process",
      "fileName": "AbstractProcessorLight.java",
      "lineNumber": 65,
      "nativeMethod": false,
      "className": "org.apache.coyote.AbstractProcessorLight"
    },
    {
      "classLoaderName": "app",
      "methodName": "process",
      "fileName": "AbstractProtocol.java",
      "lineNumber": 893,
      "nativeMethod": false,
      "className": "org.apache.coyote.AbstractProtocol$ConnectionHandler"
    },
    {
      "classLoaderName": "app",
      "methodName": "doRun",
      "fileName": "NioEndpoint.java",
      "lineNumber": 1726,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.net.NioEndpoint$SocketProcessor"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "SocketProcessorBase.java",
      "lineNumber": 49,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.net.SocketProcessorBase"
    },
    {
      "classLoaderName": "app",
      "methodName": "runWorker",
      "fileName": "ThreadPoolExecutor.java",
      "lineNumber": 1191,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.ThreadPoolExecutor"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "ThreadPoolExecutor.java",
      "lineNumber": 659,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker"
    },
    {
      "classLoaderName": "app",
      "methodName": "run",
      "fileName": "TaskThread.java",
      "lineNumber": 61,
      "nativeMethod": false,
      "className": "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable"
    },
    {
      "moduleName": "java.base",
      "moduleVersion": "17.0.2",
      "methodName": "run",
      "fileName": "Thread.java",
      "lineNumber": 833,
      "nativeMethod": false,
      "className": "java.lang.Thread"
    }
  ],
  "type": "about:blank",
  "title": "Unauthorized",
  "status": "UNAUTHORIZED",
  "detail": "Full authentication is required to access this resource",
  "message": "Unauthorized: Full authentication is required to access this resource",
  "localizedMessage": "Unauthorized: Full authentication is required to access this resource"
}
For value "hello%there", I send GET /api/v1/echo/hello%25there and I receive the same as for semicolon.
Any other special character seems to be correctly decoded by Spring, but not these 3 ones.
Am I missing something ?
Is there any good way to achieve this, without having to tell spring "hey don't decode the path variables, I will do it by myself" and without having to mess with security configuration (as mentioned in https://www.baeldung.com/spring-slash-character-in-url) ?
 
    
 
     
    