caddyhttp: Sanitize the path before evaluating path matchers (#4407)

This commit is contained in:
Francis Lavoie 2021-11-08 15:45:03 -05:00 committed by GitHub
parent f376a38b25
commit e7457b43e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 72 additions and 4 deletions

View file

@ -21,6 +21,7 @@ import (
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url" "net/url"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
@ -314,7 +315,15 @@ func (m MatchPath) Provision(_ caddy.Context) error {
// Match returns true if r matches m. // Match returns true if r matches m.
func (m MatchPath) Match(r *http.Request) bool { func (m MatchPath) Match(r *http.Request) bool {
lowerPath := strings.ToLower(r.URL.Path) // PathUnescape returns an error if the escapes aren't
// well-formed, meaning the count % matches the RFC.
// Return early if the escape is improper.
unescapedPath, err := url.PathUnescape(r.URL.Path)
if err != nil {
return false
}
lowerPath := strings.ToLower(unescapedPath)
// see #2917; Windows ignores trailing dots and spaces // see #2917; Windows ignores trailing dots and spaces
// when accessing files (sigh), potentially causing a // when accessing files (sigh), potentially causing a
@ -323,6 +332,16 @@ func (m MatchPath) Match(r *http.Request) bool {
// being matched by *.php to be treated as PHP scripts // being matched by *.php to be treated as PHP scripts
lowerPath = strings.TrimRight(lowerPath, ". ") lowerPath = strings.TrimRight(lowerPath, ". ")
// Clean the path, merges doubled slashes, etc.
// This ensures maliciously crafted requests can't bypass
// the path matcher. See #4407
lowerPath = path.Clean(lowerPath)
// Cleaning may remove the trailing slash, but we want to keep it
if lowerPath != "/" && strings.HasSuffix(r.URL.Path, "/") {
lowerPath = lowerPath + "/"
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
for _, matchPath := range m { for _, matchPath := range m {
@ -396,7 +415,26 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m. // Match returns true if r matches m.
func (m MatchPathRE) Match(r *http.Request) bool { func (m MatchPathRE) Match(r *http.Request) bool {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
return m.MatchRegexp.Match(r.URL.Path, repl)
// PathUnescape returns an error if the escapes aren't
// well-formed, meaning the count % matches the RFC.
// Return early if the escape is improper.
unescapedPath, err := url.PathUnescape(r.URL.Path)
if err != nil {
return false
}
// Clean the path, merges doubled slashes, etc.
// This ensures maliciously crafted requests can't bypass
// the path matcher. See #4407
cleanedPath := path.Clean(unescapedPath)
// Cleaning may remove the trailing slash, but we want to keep it
if cleanedPath != "/" && strings.HasSuffix(r.URL.Path, "/") {
cleanedPath = cleanedPath + "/"
}
return m.MatchRegexp.Match(cleanedPath, repl)
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.

View file

@ -257,6 +257,21 @@ func TestPathMatcher(t *testing.T) {
input: "/foo/BAR.txt", input: "/foo/BAR.txt",
expect: true, expect: true,
}, },
{
match: MatchPath{"/foo*"},
input: "//foo/bar",
expect: true,
},
{
match: MatchPath{"/foo*"},
input: "//foo",
expect: true,
},
{
match: MatchPath{"/foo*"},
input: "/%2F/foo",
expect: true,
},
{ {
match: MatchPath{"*"}, match: MatchPath{"*"},
input: "/", input: "/",
@ -326,15 +341,30 @@ func TestPathREMatcher(t *testing.T) {
expect: true, expect: true,
}, },
{ {
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}}, match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
input: "/foo", input: "/foo",
expect: true, expect: true,
}, },
{ {
match: MatchPathRE{MatchRegexp{Pattern: "/foo"}}, match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
input: "/foo/", input: "/foo/",
expect: true, expect: true,
}, },
{
match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
input: "//foo",
expect: true,
},
{
match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
input: "//foo/",
expect: true,
},
{
match: MatchPathRE{MatchRegexp{Pattern: "^/foo"}},
input: "/%2F/foo/",
expect: true,
},
{ {
match: MatchPathRE{MatchRegexp{Pattern: "/bar"}}, match: MatchPathRE{MatchRegexp{Pattern: "/bar"}},
input: "/foo/", input: "/foo/",