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) ?