@marcel91200 / caddy Mirror · read-only / commits / c645c2b4
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

rewrite: fix strip_path_suffix decoding a literal '%' as an escape (#8005)

Nguyen Thanh Dat committed Nov 3, 2026 at 13:31 UTC c645c2b43928896ab8deeb555c69fd36fc6bd4d8
2 files changed +51 -2
modules/caddyhttp/rewrite/rewrite.go
+5 -2
@@ -507,9 +507,12 @@ func trimPathSuffix(escapedPath, suffix string) string {
507
508 // if escapedPath uses a percent-encoding that ends at this position but
509 // the suffix pattern does not encode this position, decode escapedPath's
510 - // escape so the comparison happens in normalized/unescaped space
510 + // escape so the comparison happens in normalized/unescaped space.
511 + // Walking right-to-left, the suffix is inside one of its own escapes
512 + // either when an escape ends here (a '%' two bytes back) or when the
513 + // cursor has reached that escape's introducing '%' itself.
514 pathHasEscape := iPath >= 3 && escapedPath[iPath-3] == '%'
512 - suffixHasEscape := iSuffix >= 3 && suffix[iSuffix-3] == '%'
515 + suffixHasEscape := (iSuffix >= 3 && suffix[iSuffix-3] == '%') || suffixCh == '%'
516 if pathHasEscape && !suffixHasEscape {
517 decoded, err := url.PathUnescape(escapedPath[iPath-3 : iPath])
518 if err != nil {
modules/caddyhttp/rewrite/rewrite_test.go
+46
@@ -361,6 +361,52 @@ func TestRewrite(t *testing.T) {
361 input: newRequest(t, "GET", "/foo/bar/suffix"),
362 expect: newRequest(t, "GET", "/foo/bar/suffix"),
363 },
364 + {
365 + // the suffix pattern's own '%' must be compared literally: here the
366 + // path's "%25" decodes to a literal '%', which is not the escape
367 + // introducer the pattern asks for, so nothing may be stripped
368 + rule: Rewrite{StripPathSuffix: "%41"},
369 + input: newRequest(t, "GET", "/file%2541"), // decodes to "/file%41"
370 + expect: newRequest(t, "GET", "/file%2541"),
371 + },
372 + {
373 + rule: Rewrite{StripPathSuffix: "%2f"},
374 + input: newRequest(t, "GET", "/file%252f"), // decodes to "/file%2f"
375 + expect: newRequest(t, "GET", "/file%252f"),
376 + },
377 + {
378 + rule: Rewrite{StripPathSuffix: "%25"},
379 + input: newRequest(t, "GET", "/file%2525"), // decodes to "/file%25"
380 + expect: newRequest(t, "GET", "/file%2525"),
381 + },
382 + {
383 + // ...but a path that really does use the pattern's escape is stripped
384 + rule: Rewrite{StripPathSuffix: "%41"},
385 + input: newRequest(t, "GET", "/file%41"),
386 + expect: newRequest(t, "GET", "/file"),
387 + },
388 + {
389 + rule: Rewrite{StripPathSuffix: "%2f"},
390 + input: newRequest(t, "GET", "/file%2f"),
391 + expect: newRequest(t, "GET", "/file"),
392 + },
393 + {
394 + rule: Rewrite{StripPathSuffix: "%25"},
395 + input: newRequest(t, "GET", "/file%25"),
396 + expect: newRequest(t, "GET", "/file"),
397 + },
398 + {
399 + // an escaped suffix pattern must not match the decoded character
400 + rule: Rewrite{StripPathSuffix: "%2f"},
401 + input: newRequest(t, "GET", "/file/"),
402 + expect: newRequest(t, "GET", "/file/"),
403 + },
404 + {
405 + // a decoded suffix pattern still matches the path's escaped form
406 + rule: Rewrite{StripPathSuffix: ".html"},
407 + input: newRequest(t, "GET", "/a%2ehtml"), // %2e == '.'
408 + expect: newRequest(t, "GET", "/a"),
409 + },
410
411 {
412 rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},