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: treat only valid percent encodings as escapes (#8345)
Zen Dodd committed
Nov 3, 2026 at 18:11 UTC
43653cfb6594890ba71dc55386e5ca5a04d97c29
2 files changed
+41
-2
modules/caddyhttp/rewrite/rewrite.go
+11
-2
@@ -456,7 +456,7 @@ func trimPathPrefix(escapedPath, prefix string) string {
456
prefixCh := prefix[iPrefix]
457
ch := string(escapedPath[iPath])
458
459
- if ch == "%" && prefixCh != '%' && len(escapedPath) >= iPath+3 {
459
+ if ch == "%" && !hasValidPercentEncoding(prefix, iPrefix) && len(escapedPath) >= iPath+3 {
460
var err error
461
ch, err = url.PathUnescape(escapedPath[iPath : iPath+3])
462
if err != nil {
@@ -512,7 +512,8 @@ func trimPathSuffix(escapedPath, suffix string) string {
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] == '%'
515
- suffixHasEscape := (iSuffix >= 3 && suffix[iSuffix-3] == '%') || suffixCh == '%'
515
+ suffixHasEscape := hasValidPercentEncoding(suffix, iSuffix-3) ||
516
+ hasValidPercentEncoding(suffix, iSuffix-1)
517
if pathHasEscape && !suffixHasEscape {
518
decoded, err := url.PathUnescape(escapedPath[iPath-3 : iPath])
519
if err != nil {
@@ -542,6 +543,14 @@ func trimPathSuffix(escapedPath, suffix string) string {
543
return escapedPath
544
}
545
546
+func hasValidPercentEncoding(s string, i int) bool {
547
+ if i < 0 || i+3 > len(s) || s[i] != '%' {
548
+ return false
549
+ }
550
+ _, err := url.PathUnescape(s[i : i+3])
551
+ return err == nil
552
+}
553
+
554
// substrReplacer describes either a simple and fast substring replacement.
555
type substrReplacer struct {
556
// A substring to find. Supports placeholders.
modules/caddyhttp/rewrite/rewrite_test.go
+30
@@ -309,6 +309,21 @@ func TestRewrite(t *testing.T) {
309
input: newRequest(t, "GET", "/a/b/c/d"),
310
expect: newRequest(t, "GET", "/a/b/c/d"),
311
},
312
+ {
313
+ rule: Rewrite{StripPathPrefix: "/%"},
314
+ input: newRequest(t, "GET", "/%25file"),
315
+ expect: newRequest(t, "GET", "/file"),
316
+ },
317
+ {
318
+ rule: Rewrite{StripPathPrefix: "/%2"},
319
+ input: newRequest(t, "GET", "/%252file"),
320
+ expect: newRequest(t, "GET", "/file"),
321
+ },
322
+ {
323
+ rule: Rewrite{StripPathPrefix: "/%zz"},
324
+ input: newRequest(t, "GET", "/%25zzfile"),
325
+ expect: newRequest(t, "GET", "/file"),
326
+ },
327
328
{
329
rule: Rewrite{StripPathSuffix: "/suffix"},
@@ -395,6 +410,21 @@ func TestRewrite(t *testing.T) {
410
input: newRequest(t, "GET", "/file%25"),
411
expect: newRequest(t, "GET", "/file"),
412
},
413
+ {
414
+ rule: Rewrite{StripPathSuffix: "%"},
415
+ input: newRequest(t, "GET", "/file%25"),
416
+ expect: newRequest(t, "GET", "/file"),
417
+ },
418
+ {
419
+ rule: Rewrite{StripPathSuffix: "%2"},
420
+ input: newRequest(t, "GET", "/file%252"),
421
+ expect: newRequest(t, "GET", "/file"),
422
+ },
423
+ {
424
+ rule: Rewrite{StripPathSuffix: "%zz"},
425
+ input: newRequest(t, "GET", "/file%25zz"),
426
+ expect: newRequest(t, "GET", "/file"),
427
+ },
428
{
429
// an escaped suffix pattern must not match the decoded character
430
rule: Rewrite{StripPathSuffix: "%2f"},