diff --git a/src/prologue/core/route.nim b/src/prologue/core/route.nim index e494709aa..f2b5d168a 100644 --- a/src/prologue/core/route.nim +++ b/src/prologue/core/route.nim @@ -127,6 +127,19 @@ func newRouter*(): Router {.inline.} = template isInvalidPath(path: string): bool = path.allCharsInSet(allowedCharsInPattern) +func containsEncodedPathSeparator(value: string): bool {.inline.} = + ## Prevent a non-greedy route parameter from gaining another path segment + ## when `getPathParams` URL-decodes it after routing. + var index = 0 + while index + 2 < value.len: + if value[index] == '%': + let high = value[index + 1] + let low = value[index + 2] + if (high == '2' and low in {'f', 'F'}) or + (high == '5' and low in {'c', 'C'}): + return true + inc index + func ensureCorrectRoute( path: string ): string {.raises: [].} = @@ -430,12 +443,14 @@ func matchTree( else: let newPathIndex = path.find(pathSeparator, pathIndex) # skip forward to the next separator - if newPathIndex == -1: - ctx.request.pathParams[node.value] = path[pathIndex .. ^1] - pathIndex = path.len - else: - ctx.request.pathParams[node.value] = path[pathIndex .. newPathIndex - 1] - pathIndex = newPathIndex + let paramValue = if newPathIndex == -1: + path[pathIndex .. ^1] + else: + path[pathIndex .. newPathIndex - 1] + if paramValue.containsEncodedPathSeparator: + break matching + ctx.request.pathParams[node.value] = paramValue + pathIndex = if newPathIndex == -1: path.len else: newPathIndex if pathIndex == path.len and node.isTerminator: # the path was exhausted and we reached a node that has a handler return some(node.handler) diff --git a/tests/mock/tmock_mocking/tmock_route.nim b/tests/mock/tmock_mocking/tmock_route.nim index d0313448a..0eedb7b2c 100644 --- a/tests/mock/tmock_mocking/tmock_route.nim +++ b/tests/mock/tmock_mocking/tmock_route.nim @@ -86,6 +86,69 @@ block Basic_Mapping: let ctx = testContext(app, "/value") doAssert ctx.getPathParams("param") == "value" + # Encoded separators must not turn a single-segment parameter into a path. + block: + let routeCases = [ + (route: "/{param}", prefix: "/", suffix: ""), + (route: "/files/{param}", prefix: "/files/", suffix: ""), + (route: "/files/{param}/metadata", prefix: "/files/", suffix: "/metadata") + ] + let encodedSeparators = ["%2F", "%2f", "%5C", "%5c", "%%2F", "%%5C"] + + for httpMethod in [HttpGet, HttpPost]: + for routeCase in routeCases: + for separator in encodedSeparators: + for paramValue in [separator & "outside.txt", + ".." & separator & "outside.txt", + "outside.txt" & separator]: + var app = prepareApp() + app.addTestRoute(routeCase.route, httpMethod) + let ctx = testFailedContext( + app, + routeCase.prefix & paramValue & routeCase.suffix, + httpMethod + ) + doAssert not ctx.request.pathParams.hasKey("param") + + # Ordinary, double, incomplete, and invalid encodings remain single segments. + block: + let encodedValues = [ + (encoded: "hello%20world", decoded: "hello world"), + (encoded: "%2E%2E", decoded: ".."), + (encoded: "value%25done", decoded: "value%done"), + (encoded: "caf%C3%A9", decoded: "caf\xC3\xA9"), + (encoded: "..%252Foutside.txt", decoded: "..%2Foutside.txt"), + (encoded: "..%255Coutside.txt", decoded: "..%5Coutside.txt"), + (encoded: "percent%", decoded: "percent%"), + (encoded: "short%2", decoded: "short%2"), + (encoded: "invalid%2G", decoded: "invalid%2G"), + (encoded: "invalid%GG", decoded: "invalid%GG") + ] + + for httpMethod in [HttpGet, HttpPost]: + for value in encodedValues: + var app = prepareApp() + app.addTestRoute("/files/{param}", httpMethod) + let ctx = testContext(app, "/files/" & value.encoded, httpMethod) + doAssert ctx.getPathParams("param") == value.decoded + + # Greedy parameters explicitly accept multiple path segments. + block: + let pathValues = [ + (encoded: "foo/bar", decoded: "foo/bar"), + (encoded: "foo%2Fbar", decoded: "foo/bar"), + (encoded: "foo%2fbar", decoded: "foo/bar"), + (encoded: "foo%5Cbar", decoded: "foo\\bar"), + (encoded: "foo%5cbar", decoded: "foo\\bar") + ] + + for httpMethod in [HttpGet, HttpPost]: + for value in pathValues: + var app = prepareApp() + app.addTestRoute("/files/{param}$", httpMethod) + let ctx = testContext(app, "/files/" & value.encoded, httpMethod) + doAssert ctx.getPathParams("param") == value.decoded + # test "Wildcard in middle": block: var app = prepareApp()