diff --git a/cgi.go b/cgi.go index 427fcdab71..fedf1fe84f 100644 --- a/cgi.go +++ b/cgi.go @@ -322,17 +322,22 @@ func cleanRequestPath(reqPath string, windows bool) string { } // escapedPathSelectsAnotherScript reports whether cleaning the escaped path, as Caddy's path matchers do for patterns containing "%", leads to another script than cleaning the decoded one. -func escapedPathSelectsAnotherScript(u *url.URL, splitPath []string) bool { - if u.RawPath == "" { +func escapedPathSelectsAnotherScript(u *url.URL, splitPath []string, windows bool) bool { + // on Windows, those matchers read a backslash, escaped even without RawPath, as an escaped separator + backslash := windows && strings.Contains(u.Path, `\`) + if u.RawPath == "" && !backslash { return false } - windows := runtime.GOOS == "windows" escaped, err := url.PathUnescape(cleanRequestPath(u.EscapedPath(), windows)) if err != nil { return true } + if backslash { + escaped = strings.ReplaceAll(escaped, `\`, "/") + } + if splitPath == nil { splitPath = []string{".php"} } diff --git a/cgi_test.go b/cgi_test.go index 21ecde7b22..a046952d04 100644 --- a/cgi_test.go +++ b/cgi_test.go @@ -277,7 +277,36 @@ func TestEscapedPathSelectsAnotherScript(t *testing.T) { if !assert.NoError(t, err) { return } - assert.Equal(t, tt.expected, escapedPathSelectsAnotherScript(u, nil)) + assert.Equal(t, tt.expected, escapedPathSelectsAnotherScript(u, nil, false)) + }) + } +} + +func TestEscapedPathSelectsAnotherScriptWindows(t *testing.T) { + t.Parallel() + + tests := []struct { + target string + expected bool + }{ + {`/dir\index.php`, false}, + {`/dir\index.php\info`, false}, + {"/dir%5Cindex.php/info", false}, + {"/index.php/a%5C..%5Cb", false}, + {`/public/tool.php\..\..\admin\tool.php`, true}, + {"/public/tool.php%5C..%5C..%5Cadmin%5Ctool.php", true}, + {"/public/tool.php%2f..%2f..%2fadmin%2ftool.php", true}, + } + + for _, tt := range tests { + t.Run(tt.target, func(t *testing.T) { + t.Parallel() + + u, err := url.ParseRequestURI(tt.target) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, tt.expected, escapedPathSelectsAnotherScript(u, nil, true)) }) } } diff --git a/context.go b/context.go index b36e82a543..07f58ef599 100644 --- a/context.go +++ b/context.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" "strconv" "strings" "time" @@ -212,7 +213,7 @@ func (fc *frankenPHPContext) closeContext() { // validate checks if the request should be outright rejected func (fc *frankenPHPContext) validate() error { - if strings.Contains(fc.request.URL.Path, "\x00") || escapedPathSelectsAnotherScript(fc.request.URL, fc.splitPath) { + if strings.Contains(fc.request.URL.Path, "\x00") || escapedPathSelectsAnotherScript(fc.request.URL, fc.splitPath, runtime.GOOS == "windows") { fc.reject(ErrInvalidRequestPath) return ErrInvalidRequestPath