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"}}},