Merge commit from fork

* fix: resolve dot segments before checking path rules (GHSA-chxv-mvjv-f92j)

* fix: match trailing-slash path rules against the bare collection

* fix: match destination rules against the URL path when no prefix is set

* fix: restrict collections named by a rule without granting access to them

* docs: cleanup
This commit is contained in:
Henrique Dias
2026-08-05 10:18:13 +02:00
committed by GitHub
parent c04649bf40
commit f869dd6276
5 changed files with 323 additions and 34 deletions
+37 -10
View File
@@ -4,9 +4,39 @@ import (
"errors"
"net/http"
"net/url"
"path"
"strings"
)
// cleanPath resolves dot segments so that the permission checks see the same
// path that the backing file system will ultimately open. The file systems in
// golang.org/x/net/webdav apply path.Clean before joining the backing
// directory, so without this the two layers disagree on which file a request
// names and a rule can be bypassed with e.g. "/public/../secret/file.txt".
func cleanPath(p string) string {
if !strings.HasPrefix(p, "/") {
p = "/" + p
}
cleaned := path.Clean(p)
// path.Clean drops the trailing slash, but rules are prefix matches and are
// commonly written with one, such as "/c/". Dropping it would stop a request
// for the collection itself from matching the rule that names it.
if cleaned != "/" && isCollectionPath(p) {
cleaned += "/"
}
return cleaned
}
// isCollectionPath reports whether p names a collection rather than a resource
// within it. Besides an explicit trailing slash, a trailing "." or ".." segment
// also resolves to the collection itself.
func isCollectionPath(p string) bool {
return strings.HasSuffix(p, "/") || strings.HasSuffix(p, "/.") || strings.HasSuffix(p, "/..")
}
type request struct {
method string
path string
@@ -24,6 +54,11 @@ func newRequest(r *http.Request, prefix string) (*request, error) {
return nil, errors.New("invalid destination header")
}
// RFC 4918, section 10.3, has Destination as an absolute URI, which is
// what clients send in practice. Only the path is relevant here, and
// taking it unconditionally keeps the host out of the matched value.
destination = u.Path
if prefix != "" {
destination = strings.TrimPrefix(u.Path, prefix)
if len(destination) >= len(u.Path) {
@@ -31,11 +66,7 @@ func newRequest(r *http.Request, prefix string) (*request, error) {
}
}
if !strings.HasPrefix(destination, "/") {
destination = "/" + destination
}
ctx.destination = destination
ctx.destination = cleanPath(destination)
}
path := r.URL.Path
@@ -47,11 +78,7 @@ func newRequest(r *http.Request, prefix string) (*request, error) {
}
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
ctx.path = path
ctx.path = cleanPath(path)
return ctx, nil
}