GHSA-38XV-HF3P-H7MQ
Vulnerability from github – Published: 2026-09-10 22:45 – Updated: 2026-09-10 22:45Summary
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.Joincollapses..on the standard (ASCII) form before encoding is applied (e.g.FromStandardPath(path.Join(...))atbackend/b2/b2.go:1641), soEncodeDotnever gets the chance to neutralize the...-
lib/bucket.Joindoes not clean paths (keeps..as a literal key segment);path.Joindoes.backend/s3,backend/azureblob,backend/googlecloudstoragealready usebucket.Joinand 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 isencoder.Display(==Standard), andFromStandardPathshort-circuits to a pass-through in that mode, so..survives;f.absRootis absolute, sopath.Join("/home/user/root", "../../../../etc/passwd")->/etc/passwd. - webdav:
filePathatbackend/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:
- the source is a flat-keyspace object store (not a filesystem - a local/sftp/smb source cannot represent
../../xas one directory entry), and - the offending key was written with native, non-rclone tooling - rclone's own writer applies
EncodeDotand 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
# 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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/rclone/rclone"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.75.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-88046"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-10T22:45:36Z",
"nvd_published_at": "2026-09-10T17:17:08Z",
"severity": "MODERATE"
},
"details": "### Summary\n\nMultiple backends, when given a specially crafted object to copy, can escape the backend confinement.\n\n| Backend | Keep/Close | Per-backend severity |\n|---|---|---|\n| sftp | Medium | Real filesystem escape, fires under default encoding. |\n| smb | Low-Medium | Escapes to a different SMB share the credential can reach. |\n| ftp | Low | Real, leading-`..` overshoot PoC is partly neutralized by encoding; escape bounded to at/below the login base. |\n| webdav | Low | Server-side ACLs are the real boundary. |\n| b2 | Low | Same-account sibling **bucket** crossing on a flat keyspace. |\n| swift | Low | Same, container. |\n| qingstor | Low | Same. |\n| oracleobjectstorage | Low | Same. |\n| internetarchive | Low | IA items are owner-writable only; confined to user\u0027s own items. |\n| storj | Low | Can retarget a different bucket in the same access grant. |\n| filelu | Low | Confined to the user\u0027s own account. |\n| shade | Low | Confined to the user\u0027s own drive. |\n| sia | Low | siad API password already grants full-daemon access. |\n\n## Root cause\n\nrclone core does **not** sanitize `..` in a source object\u0027s `Remote()` - verified: nothing in `fs/march`, `fs/sync`, `fs/list`, or `fs/operations` rejects `..` segments before the name reaches the destination backend\u0027s `Put`/`Update`/`Mkdir`. Confinement is therefore each backend\u0027s responsibility, and these backends join `root + remote` without a check.\n\nThis divides into two classes:\n\n- **Bucket based backends** - `bucket.Split(path.Join(f.root, rootRelativePath))`:\n - `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`.\n - `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 `..`.\n - `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.\n\n- **Path based backends** - `path.Join(root, remote)` onto a real path:\n - 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\")` -\u003e `/etc/passwd`.\n - webdav: `filePath` at `backend/webdav/webdav.go:426-432`.\n - ftp: `path.Join(f.root, remote)` at ~14 sites (e.g. `backend/ftp/ftp.go:1247`).\n - filelu, shade, sia: analogous joins.\n\n### Precondition that limits reachability\n\nFor any of these to fire, a **source** must hand rclone a `Remote()` containing raw `..`. That is only possible when:\n\n1. the source is a **flat-keyspace object store** (not a filesystem - a local/sftp/smb source cannot represent `../../x` as one directory entry), **and**\n2. the offending key was written with **native, non-rclone tooling** - rclone\u0027s own writer applies `EncodeDot` and rewrites a `..` segment to fullwidth `\uff0e\uff0e`, so you cannot create such a key *through rclone*.\n\nrclone\u0027s 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.\n\n### Example attack\n\n```bash\n# Step 1 - attacker, using NATIVE S3 tooling (NOT rclone) on a source the victim ingests from:\naws s3api put-object --bucket shared-drop --key \u0027../../victim-backups/pwned.txt\u0027 --body evil.txt\n\n# Step 2 - victim\u0027s ordinary ingest:\nrclone copy s3-drop:shared-drop b2:victim-uploads/incoming\n# path.Join(\"victim-uploads/incoming\", \"../../victim-backups/pwned.txt\") = \"victim-backups/pwned.txt\"\n# -\u003e lands in the victim\u0027s victim-backups bucket instead of under incoming/\n```\n\nThe blast radius is the victim\u0027s **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\u0027s own permissions.\n\n## Precedent\n\nThis is the same class as the already-fixed local backend advisory [https://github.com/rclone/rclone/security/advisories/GHSA-7p4m-qxvv-g567](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\u0027s own OS filesystem; the same reasoning extends (at lower severity) to sftp/smb.",
"id": "GHSA-38xv-hf3p-h7mq",
"modified": "2026-09-10T22:45:36Z",
"published": "2026-09-10T22:45:36Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/security/advisories/GHSA-38xv-hf3p-h7mq"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-88046"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/commit/57842c5ee4e1407eda06a414a36510cce2db4252"
},
{
"type": "PACKAGE",
"url": "https://github.com/rclone/rclone"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/releases/tag/v1.75.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "rclone: source object names can escape the configured root on upload"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.