@marcel91200 / caddy Mirror · read-only / commits / 43653cfb
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: 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"},