GitHub Advisoryadvisory · Go
GHSA-38xv-hf3p-h7mq rclone: source object names can escape the configured root on upload
MEDIUM
Official page ↗
- Aliases
- CVE-2026-88046
- Ecosystem
- Go
- Packages
- github.com/rclone/rclone
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N
- Weaknesses
- CWE-22
- Reviewed
- 2026-09-10
Affected packages
• Go github.com/rclone/rclone: < 1.75.1 → fixed in 1.75.1
Description
Summary
Multiple backends, when given a specially crafted object to copy, can escape the backend confinement.
| Backend | Keep/Close | Per-backend severity |
|---|---|---|
| sftp | Medium | Real filesystem escape, fires under default encoding. |
| smb | Low-Medium | Escapes to a different SMB share the credential can reach. |
| ftp | Low | Real, leading-`..` overshoot PoC is partly neutralized by encoding; escape bounded to at/below the login base. |
| webdav | Low | Server-side ACLs are the real boundary. |
| b2 | Low | Same-account sibling bucket crossing on a flat keyspace. |
| swift | Low | Same, container. |
| qingstor | Low | Same. |
| oracleobjectstorage | Low | Same. |
| internetarchive | Low | IA items are owner-writable only; confined to user's own items. |
| storj | Low | Can retarget a different bucket in the same access grant. |
| filelu | Low | Confined to the user's own account. |
| shade | Low | Confined to the user's own drive. |
| sia | Low | siad API password already grants full-daemon access. |
Root cause
rclone core does not sanitize `..` in a source object's `Remote()` - verified: nothing in `fs/march`, `fs/sync`, `fs/list`, or `fs/operations` rejects `..` segments before the name reaches the destination backend's `Put`/`Update`/`Mkdir`. Confinement is therefore each backend's responsibility, and these backends join `root + remote` without a check.
This divides into two classes:
• Bucket based backends - `bucket.Split(path.Join(f.root, rootRelativePath))`:
• `backend/b2/b2.go:404`, `backend/swift/swift.go:464`, `backend/qingstor/qingstor.go:198`, `backend/oracleobjectstorage/oracleobjectstorage.go:245`, `backend/internetarchive/internetarchive.go:1016`, `backend/smb/smb.go:885`, `backend/storj/fs.go:289`.
• `path.Join` collapses `..` on the standard (ASCII) form before encoding is applied (e.g. `FromStandardPath(path.Join(...))` at `backend/b2/b2.go:1641`), so `EncodeDot` never gets the chance to neutralize the `..`.
• `lib/bucket.Join` does not clean paths (keeps `..` as a literal key segment); `path.Join` does. `backend/s3`, `backend/azureblob`, `backend/googlecloudstorage` already use `bucket.Join` and are therefore not affected.
• Path based backends - `path.Join(root, remote)` onto a real path:
• sftp: `remotePath = path.Join(f.absRoot, f.opt.Enc.FromStandardPath(remote))` (`backend/sftp/sftp.go:2497`). Default encoding is `encoder.Display` (== `Standard`), and `FromStandardPath` short-circuits to a pass-through in that mode, so `..` survives; `f.absRoot` is absolute, so `path.Join("/home/user/root", "../../../../etc/passwd")` -> `/etc/passwd`.
• webdav: `filePath` at `backend/webdav/webdav.go:426-432`.
• ftp: `path.Join(f.root, remote)` at ~14 sites (e.g. `backend/ftp/ftp.go:1247`).
• filelu, shade, sia: analogous joins.
Precondition that limits reachability
For any of these to fire, a source must hand rclone a `Remote()` containing raw `..`. That is only possible when:
1. the source is a flat-keyspace object store (not a filesystem - a local/sftp/smb source cannot represent `../../x` as one directory entry), and
2. the offending key was written with native, non-rclone tooling - rclone's own writer applies `EncodeDot` and rewrites a `..` segment to fullwidth `..`, so you cannot create such a key *through rclone*.
rclone's source-side listing does pass a natively-planted raw `..` key through unchanged (verified for b2: `remote := file.Name[len(prefix):]` after `ToStandardPath`, `backend/b2/b2.go:858,867`). The reports never establish this precondition; it is the same omission across every member of the class.
Example attack
```bash
Step 1 - attacker, using NATIVE S3 tooling (NOT rclone) on a source the victim ingests from:
aws s3api put-object --bucket shared-drop --key '../../victim-backups/pwned.txt' --body evil.txt
Step 2 - victim's ordinary ingest:
rclone copy s3-drop:shared-drop b2:victim-uploads/incoming
path.Join("victim-uploads/incoming", "../../victim-backups/pwned.txt") = "victim-backups/pwned.txt"
-> lands in the victim's victim-backups bucket instead of under incoming/
```
The blast radius is the victim's own account (a bucket/share/path the configured credential already reaches) - integrity misdirection, not a cross-tenant or confidentiality breach. sftp/smb are the exception in *reach* (server filesystem / other share), still bounded by the login's own permissions.
Precedent
This is the same class as the already-fixed local backend advisory https://github.com/rclone/rclone/security/advisories/GHSA-7p4m-qxvv-g567, which added `(*Fs).localPath` returning `errPathEscapes` for names resolving outside the root (`backend/local/local.go:819-826`). That fix was justified because the destination was the operator's own OS filesystem; the same reasoning extends (at lower severity) to sftp/smb.
Related
Alias · CVE (1)
- CVE-2026-88046 rclone is a command-line program to sync files and directories to and from different cloud storage providers. vulnerability
Weakness · CWE (1)
- CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness
References
- https://github.com/rclone/rclone/security/advisories/GHSA-38xv-hf3p-h7mq web
- https://nvd.nist.gov/vuln/detail/CVE-2026-88046 advisory
- https://github.com/rclone/rclone/commit/57842c5ee4e1407eda06a414a36510cce2db4252 web
- https://github.com/rclone/rclone package
- https://github.com/rclone/rclone/releases/tag/v1.75.1 web