2

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

Dartz
  • 165
  • 12
  • Does this answer your question? [How to Pass URL as Path Variable](https://stackoverflow.com/questions/57212145/how-to-pass-url-as-path-variable) – UFO_Rider Oct 03 '22 at 16:09

3 Answers3

2

For Getmapping you can prefer this way.

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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/api/v1")
public class Controller {

    @GetMapping("echo/**")
    public ResponseEntity<String> echo(HttpServletRequest request) {
        String fullUrl = request.getRequestURL().toString();
        String url = fullUrl.split("/echo/")[1];
        System.out.println(url);
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(url);
    }

}

enter image description here

UFO_Rider
  • 111
  • 2
  • 12
  • I have the same problem with endpoints using POST method too – Dartz Oct 03 '22 at 14:48
  • I've edited the comment. check now and compare with your code @Dartz – UFO_Rider Oct 03 '22 at 15:06
  • Changing the @PathVariable to @RequestParam would work, but that means I need to change everything like `GET /api/v1/resources/{resourceId}/sub-resources/{subResourceId}` to something like `POST /api/v1/resources/_get`. Almost all of my endpoints would looks like this with this workaround. I guess that's not very conventional ? – Dartz Oct 03 '22 at 15:22
  • 1
    @Dartz, @UFO_Rider changing from `@Get` into `@Post` will not provide anything valuable to the questioners requirement – Panagiotis Bougioukos Oct 03 '22 at 15:39
  • Updated the answer please verify it now @Dartz – UFO_Rider Oct 03 '22 at 15:41
  • Fixed the code @PanagiotisBougioukos thanks for the suggestion – UFO_Rider Oct 03 '22 at 15:47
  • @UFO_Rider my above code example was really simplified, in fact the real API has to be used in Swagger-UI, but with this change I lost the ability to specify a parameter – Dartz Oct 03 '22 at 15:50
  • @UFO_Rider, @Dartz the above solution will not work good in case the URL to be tested is `"localhost:8080/api/v1/echo/hello/there/echo/some/additional/value"` – Panagiotis Bougioukos Oct 03 '22 at 16:03
  • @PanagiotisBougioukos fullUrl.split("/echo/")[1]; gives you the 1st param. you need to get the value based on your conditions:) – UFO_Rider Oct 03 '22 at 16:08
  • 1
    @UFO_Rider I mean that you expect the path to be dynamic and it can also contain the value to be splitted inside like `echo` which then would lead to the value need much processing to be error free. Query parameter otherwise would just hand to you the dynamic value provided without much processing and complications like the above I have mentioned. The questioners example `echo/{value}` means he expects a dynamic url to be matched and he want to extract the value into a simple string. – Panagiotis Bougioukos Oct 03 '22 at 16:16
1

Trying to achieve what you ask, you will have to do some deep configurations in tomcat and/or probably some custom rewrite rules. All these could easily backfire by creating malfunctions on uri matcher of your application or even worse create security malfunctions.

The easiest way here to move forward is to transform your controllers from expecting this string with special characters as a @PathVariable into a query parameter with @RequestParam.

So instead of using the

    @GetMapping("echo/{value}")
    public ResponseEntity<String> echo(@PathVariable("value") String value) {
        return ResponseEntity
                .status(HttpStatus.OK)
                .contentType(MediaType.TEXT_XML)
                .body(value);
    }

you might change into

   @GetMapping("echo/")
    public ResponseEntity<String> echo(@RequestParam("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

Then try with GET /api/v1/echo?value=hello%2Fthere

Panagiotis Bougioukos
  • 15,955
  • 2
  • 30
  • 47
  • UFO_Rider's edited answer suggested the same, but that means I need to change everything like `GET /api/v1/resources/{resourceId}/sub-resources/{subResourceId}` to something like `POST /api/v1/resources/_get`. Isn't it a bad practice ? – Dartz Oct 03 '22 at 15:34
  • 1
    @Dartz there is no need to change from `@Get` into `@Post` therefore I made an additional answer to provide a specific solution and some elaboration on the risk of what you initially asked in the question – Panagiotis Bougioukos Oct 03 '22 at 15:37
  • Finally, I chose to do the deep configuration solution. I found one [here](https://stackoverflow.com/a/45058887/5539129) that solved my problem with @PathVariable containing slash/semicolon/percent characters. – Dartz Oct 04 '22 at 10:11
0

Did you try to use \ or ` in front of special char ?

something like hello\;there

  • When I add a backslash, I get the following error code : `HTTP Status 400 – Bad Request` with following description `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).`. So I cannot use the backslash neither (but it is not a valid character in my domain context) – Dartz Oct 03 '22 at 14:46