From c96bca12693419ac5be751a0cbbae2c3cbf89c58 Mon Sep 17 00:00:00 2001 From: Saleh Date: Sun, 26 Jul 2026 01:49:44 +0300 Subject: [PATCH] rewrite: preserve non-canonical path encoding after uri replace (#7907) changePath cleared RawPath whenever it equalled url.Path, which discarded a valid non-canonical percent-encoding produced by a replacement. This compare `RawPath` against the default escaping of Path instead. --- modules/caddyhttp/rewrite/rewrite.go | 13 +++++++++++-- modules/caddyhttp/rewrite/rewrite_test.go | 13 +++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go index c9bc061c9..8437ad790 100644 --- a/modules/caddyhttp/rewrite/rewrite.go +++ b/modules/caddyhttp/rewrite/rewrite.go @@ -513,12 +513,21 @@ func changePath(req *http.Request, newVal func(pathOrRawPath string) string) { } else { req.URL.Path = newVal(req.URL.Path) } - // RawPath is only set if it's different from the normalized Path (std lib) - if req.URL.RawPath == req.URL.Path { + // RawPath is only needed if it is a valid, non-canonical encoding of Path; + // (see #6578). Mirror net/url.URL.setPath by comparing against the default + // escaping of Path instead. + if req.URL.RawPath == defaultEscapedPath(req.URL.Path) { req.URL.RawPath = "" } } +// defaultEscapedPath returns the canonical percent-encoding of p, matching +// what net/url.URL.EscapedPath() produces when RawPath is empty. It mirrors +// the comparison net/url.URL.setPath uses to decide whether RawPath is needed. +func defaultEscapedPath(p string) string { + return (&url.URL{Path: p}).EscapedPath() +} + // queryOps describes the operations to perform on query keys: add, set, rename and delete. type queryOps struct { // Renames a query key from Key to Val, without affecting the value. diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go index 1d23aa241..0b80e45b6 100644 --- a/modules/caddyhttp/rewrite/rewrite_test.go +++ b/modules/caddyhttp/rewrite/rewrite_test.go @@ -351,6 +351,19 @@ func TestRewrite(t *testing.T) { input: newRequest(t, "GET", "/foo/findme%2Fbar"), expect: newRequest(t, "GET", "/foo/replaced%2Fbar"), }, + { + rule: Rewrite{URISubstring: []substrReplacer{{Find: "%28", Replace: "("}}}, + input: newRequest(t, "GET", "/hello/%28%29"), + expect: newRequest(t, "GET", "/hello/(%29"), + }, + { + rule: Rewrite{URISubstring: []substrReplacer{ + {Find: "%28", Replace: "("}, + {Find: "%29", Replace: ")"}, + }}, + input: newRequest(t, "GET", "/hello/%28%29"), + expect: newRequest(t, "GET", "/hello/()"), + }, { rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}},