← All advisories
CVE-2026-54563High · CVSS 7.1· CWE-863

Broken Access Control in Cloudreve WebDAV (`/dav`)

Vendor
Cloudreve
Product
Status
Published · Jul 08 2026
Researchers
riodrwn
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Published
Jul 08 2026

Summary

A Cloudreve WebDAV account stores a uri that defines the account's root folder. The WebDAV request handler (stripPrefix in pkg/webdav/webdav.go) trims the /dav prefix from the request path and joins the remainder to that root with fs.URI.JoinRaw, but never checks that the joined URI stays inside the root.

Go's net/http decodes %2e%2e to .. and %2f to / in r.URL.Path before the handler sees it, and JoinRaw resolves .. segments through the standard library's url.URL.JoinPath. A request such as GET /dav/%2e%2e/outside.txt against a credential rooted at cloudreve://my/restricted therefore resolves to cloudreve://my/outside.txt. A scoped DAV credential can read and list files outside its configured folder; a writable scoped credential can also create, overwrite, move, and delete them.

The escape stays inside the same Cloudreve user's namespace because downstream DBFS owner checks still apply. It does not cross into another user's files or onto the OS filesystem. What it breaks is the per-folder WebDAV-account boundary — the entire reason scoped DAV accounts exist (delegating limited access to a sync client or a third party).

Technical Detail

Root cause

stripPrefix joins the request suffix onto the account base with no containment check:

// pkg/webdav/webdav.go @ 54dc81d
func stripPrefix(p string, u *ent.User) (string, *fs.URI, int, error) {
	base, err := fs.NewUriFromString(u.Edges.DavAccounts[0].URI)
	if err != nil {
		return "", nil, http.StatusInternalServerError, err
	}
 
	prefix := davPrefix // "/dav"
	if r := strings.TrimPrefix(p, prefix); len(r) < len(p) {
		r = strings.TrimPrefix(r, fs.Separator)
		return r, base.JoinRaw(util.RemoveSlash(r)), http.StatusOK, nil // <-- join, no boundary check
	}
	return "", nil, http.StatusNotFound, errPrefixMismatch
}

JoinRaw splits on / and delegates to the standard library:

// pkg/filemanager/fs/uri.go @ 54dc81d
func (u *URI) JoinRaw(elem string) *URI {
	return u.Join(strings.Split(strings.TrimPrefix(elem, Separator), Separator)...)
}
 
func (u *URI) Join(elem ...string) *URI {
	newUrl, _ := url.Parse(u.U.String())
	return &URI{U: newUrl.JoinPath(lo.Map(elem, func(s string, i int) string {
		return PathEscape(s)
	})...)}
}

PathEscape leaves a . untouched (shouldEscape returns false for .), so the literal segment .. survives into url.URL.JoinPath, which cleans the path and resolves the parent reference.