Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions src/prologue/core/route.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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: [].} =
Expand Down Expand Up @@ -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)
Expand Down
63 changes: 63 additions & 0 deletions tests/mock/tmock_mocking/tmock_route.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down