diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 2bdd874b7..e10c675fb 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -640,6 +640,20 @@ func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) b iPattern++ } + // if the pattern was fully consumed before the path, the remaining path + // bytes still have to be part of the string we match against; otherwise a + // pattern with no trailing wildcard (e.g. "/foo%2fbar") would behave like a + // prefix match and incorrectly match longer paths (e.g. "/foo%2fbarbaz"). + // the pattern has no more escape hints here, so compare in normalised space. + if iPath < len(escapedPath) { + remaining := escapedPath[iPath:] + if unescaped, err := url.PathUnescape(remaining); err == nil { + sb.WriteString(unescaped) + } else { + sb.WriteString(remaining) + } + } + // we can now treat rawpath globs (%*) as regular globs (*) matchPath = strings.ReplaceAll(matchPath, "%*", "*") diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index b129aa4b7..b98743baf 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -422,6 +422,21 @@ func TestPathMatcher(t *testing.T) { input: "/ADMIN%2fPaAzZLm123NEL", expect: true, }, + { + match: MatchPath{"/foo%2fbar"}, + input: "/foo%2fbarbaz", + expect: false, + }, + { + match: MatchPath{"/foo%2f"}, + input: "/foo%2fx", + expect: false, + }, + { + match: MatchPath{"/admin%2fpanel"}, + input: "/admin%2fpanelX", + expect: false, + }, } { err := tc.match.Provision(caddy.Context{}) if err == nil && tc.provisionErr {