GHSA-F8G7-2XJC-7MFH
Vulnerability from github – Published: 2026-09-10 22:51 – Updated: 2026-09-10 22:51Summary
With -l/--links, rclone's local backend recreates a source .rclonelink object as a real symlink at the destination verbatim (preserved by design for faithful backups). Directory-metadata application, however, does not go through the os.Root sandbox and does not use NOFOLLOW syscalls. A local Directory always has translatedLink=false, so when the destination path already exists as a planted symlink, rclone applies chmod/chown/chtimes through that symlink to a target outside the destination tree. An attacker who controls the source contents (malicious/compromised remote, shared bucket) obtains attacker-valued chmod/chown/chtimes of an arbitrary path outside the backup destination.
Root Cause
MkdirMetadata(backend/local/local.go:895) callsf.lstat(=os.Lstat, local.go:465) on the destination path. On a pre-planted symlink,os.Lstatsucceeds, so theerrors.Is(err, os.ErrNotExist)branch (local.go:896) that would create a real directory via theos.Root-guardedf.Mkdiris not taken. Instead aDirectoryis built directly on the symlink path.writeMetadataToFileruns rawos.Chown(backend/local/metadata.go:131) andos.Chmod(metadata.go:158);setTimesruns rawos.Chtimes(backend/local/local.go:1318).- The CVE-2024-52522 NOFOLLOW fix (
os.Lchown/lChmod/lChtimes) is gated onif o.translatedLink(metadata.go:128/150, local.go:1315). ADirectory(newDirectory→newObjectwith no.rclonelinksuffix) is nevertranslatedLink, so it always takes the raw following branch. The CVE-2026-54572os.Rootfix covers only content writes, not metadata syscalls.
Impact
Attacker-controlled chmod/chown/chtimes (values taken from the source directory's mode/uid/gid/mtime) applied to any file or directory outside the destination. chtimes (mtime) escape works with just --links and default flags; chmod/chown escape additionally needs --metadata. When rclone runs as root with --metadata and a source uid=0, the chown primitive reaches the CVE-2024-52522 privilege-escalation ceiling (take ownership of an out-of-tree path).
Proof of Concept
mkdir -p /src /dest
# run 1: source object pwn.rclonelink whose body = /home/victim/secret.d
printf '/home/victim/secret.d' > /src/pwn.rclonelink
rclone sync --links /src /dest # plants /dest/pwn -> /home/victim/secret.d
# attacker swaps source pwn to a real directory with chosen metadata:
rm /src/pwn.rclonelink ; mkdir -p /src/pwn/keep ; chmod 777 /src/pwn
rclone sync --links --metadata /src /dest # MkdirMetadata sees /dest/pwn exists (symlink) ->
# chmod 0777 applied THROUGH it to /home/victim/secret.d
ls -ld /home/victim/secret.d # => drwxrwxrwx (outside dir, attacker-chosen mode)
A single-run PoC is achievable against directory-based object sources (drive/onedrive-class) that satisfy both ReadDirMetadata and CanHaveEmptyDirectories and can present pwn.rclonelink and pwn/ simultaneously. Local→local uses the two-run backup model (same repeated-backup model as CVE-2024-52522 and CVE-2026-54572). Verified end-to-end against the real fs/sync.Sync engine on HEAD: the two-run backup backdated the outside target's mtime and chmod'd it 0777 while os.Root correctly blocked the content-copy of pwn/keep — isolating the metadata gap.
Attack Chain
- Entry. Victim runs
rclone copy/sync --links [--metadata] <untrusted-remote>: /dest. Attacker controls source contents. - Guard: none —
--linkscopying an untrusted remote is a documented, supported operation. - Plant symlink. Source serves
pwn.rclonelinkwith body = absolute outside path; rclone recreatesdst/pwn→ outside. - Guard:
Fs.symlinkroutes creation throughos.Root.Symlink(local.go:~1552). - Bypass proof:
os.Rootcreates the link verbatim by design (commit 1154afe); the upstreamos.Rootfix's testTestSymlinkEscapeWriteThroughBlockedconfirms only write-through is refused, the link is planted. - Deferred dir-metadata fires after transfers. Source presents non-empty dir
pwn;setDelayedDirModTimes(sync.go:1002) runs strictly afterstopTransfers()(sync.go:988) — after the symlink is planted. - Guard:
MkdirMetadatawould create a real dir via os.Root-guardedf.Mkdir(local.go:897) inside itserrors.Is(err, os.ErrNotExist)branch. - Bypass proof:
os.Lstat(local.go:465) on the existing symlink returns success, so theErrNotExistbranch (local.go:896) is NOT taken;f.Mkdir/os.Root never runs. Empiricallyos.IsNotExist(err)=falsefor the planted symlink. - Sink follows the symlink.
CopyDirMetadata→MkdirMetadata→writeMetadataToFilerunsos.Chown/os.Chmod(metadata.go:131/158);DirSetModTime→setTimesrunsos.Chtimes(local.go:1318) — all ono.path="dst/pwn"withtranslatedLink=false. - Guard: CVE-2024-52522 NOFOLLOW branch (
os.Lchown/lChmod/lChtimes). - Bypass proof: that branch is gated on
if o.translatedLink(metadata.go:128/150, local.go:1315); aDirectoryalways hastranslatedLink=false, so the raw following branch runs. POSIX-confirmed:chmod 777/touchon a symlink path change the target's mode/mtime. - Impact.
chmod/chown/chtimeson an attacker-chosen path outside the destination, with attacker-controlled values.
Bypass Evidence
if o.translatedLinkgates verified verbatim on v1.75.0 at metadata.go:128/150 and local.go:1315;os.Chown/os.Chmod/os.Chtimeson the else branch at metadata.go:131/158 and local.go:1318.newDirectory→newObject(local.go:581/589/596) never sets the.rclonelinksuffix →translatedLink=falsefor all directories.MkdirMetadataskip branch:os.Lstatsucceeds on planted symlink →errors.Is(err, os.ErrNotExist)false at local.go:896 → guardedf.Mkdirskipped.- Real
fs/sync.SyncE2E on HEAD:TestDirMetadataThroughPlantedSymlink(outside dir → 0777),TestDirSetModTimeThroughPlantedSymlink(mtime set, default-on),TestE2E_TwoRunBackup(backdated outside target while content-copy blocked by os.Root). All PASS. ControlTestControl_ContentWriteBlockedconfirms harness fidelity.
Affected Versions
<= 1.75.0. Vulnerable code present on latest release tag v1.75.0 and HEAD (5629f26); git log v1.75.0..HEAD -- backend/local/metadata.go backend/local/local.go is empty (no post-release fix).
Suggested Fix
Route directory metadata through os.Root when TranslateSymlinks is set (use fchmodat(AT_SYMLINK_NOFOLLOW)/Lchown/UtimesNanoAt(AT_SYMLINK_NOFOLLOW) on the rel path within the root), and/or extend MkdirMetadata to detect that the pre-existing destination path is a symlink and refuse to apply following-metadata — mirroring the CVE-2024-52522 NOFOLLOW branch that currently exists only for translatedLink objects.
Reported by zx (Jace) — GitHub: @manus-use
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.75.0"
},
"package": {
"ecosystem": "Go",
"name": "github.com/rclone/rclone"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.75.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-88016"
],
"database_specific": {
"cwe_ids": [
"CWE-59",
"CWE-281"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-10T22:51:34Z",
"nvd_published_at": "2026-09-10T16:18:08Z",
"severity": "MODERATE"
},
"details": "## Summary\nWith `-l/--links`, rclone\u0027s local backend recreates a source `.rclonelink` object as a real symlink at the destination **verbatim** (preserved by design for faithful backups). Directory-metadata application, however, does **not** go through the `os.Root` sandbox and does **not** use NOFOLLOW syscalls. A local `Directory` always has `translatedLink=false`, so when the destination path already exists as a planted symlink, rclone applies `chmod`/`chown`/`chtimes` **through** that symlink to a target **outside** the destination tree. An attacker who controls the source contents (malicious/compromised remote, shared bucket) obtains attacker-valued `chmod`/`chown`/`chtimes` of an arbitrary path outside the backup destination.\n\n## Root Cause\n- `MkdirMetadata` (`backend/local/local.go:895`) calls `f.lstat` (=`os.Lstat`, local.go:465) on the destination path. On a pre-planted symlink, `os.Lstat` succeeds, so the `errors.Is(err, os.ErrNotExist)` branch (local.go:896) that would create a real directory via the `os.Root`-guarded `f.Mkdir` is **not** taken. Instead a `Directory` is built directly on the symlink path.\n- `writeMetadataToFile` runs raw `os.Chown` (`backend/local/metadata.go:131`) and `os.Chmod` (`metadata.go:158`); `setTimes` runs raw `os.Chtimes` (`backend/local/local.go:1318`).\n- The CVE-2024-52522 NOFOLLOW fix (`os.Lchown`/`lChmod`/`lChtimes`) is gated on `if o.translatedLink` (metadata.go:128/150, local.go:1315). A `Directory` (`newDirectory`\u2192`newObject` with no `.rclonelink` suffix) is never `translatedLink`, so it always takes the raw *following* branch. The CVE-2026-54572 `os.Root` fix covers only content **writes**, not metadata syscalls.\n\n## Impact\nAttacker-controlled `chmod`/`chown`/`chtimes` (values taken from the source directory\u0027s mode/uid/gid/mtime) applied to any file or directory **outside** the destination. `chtimes` (mtime) escape works with just `--links` and default flags; `chmod`/`chown` escape additionally needs `--metadata`. When rclone runs as root with `--metadata` and a source `uid=0`, the `chown` primitive reaches the CVE-2024-52522 privilege-escalation ceiling (take ownership of an out-of-tree path).\n\n## Proof of Concept\n```\nmkdir -p /src /dest\n# run 1: source object pwn.rclonelink whose body = /home/victim/secret.d\nprintf \u0027/home/victim/secret.d\u0027 \u003e /src/pwn.rclonelink\nrclone sync --links /src /dest # plants /dest/pwn -\u003e /home/victim/secret.d\n# attacker swaps source pwn to a real directory with chosen metadata:\nrm /src/pwn.rclonelink ; mkdir -p /src/pwn/keep ; chmod 777 /src/pwn\nrclone sync --links --metadata /src /dest # MkdirMetadata sees /dest/pwn exists (symlink) -\u003e\n # chmod 0777 applied THROUGH it to /home/victim/secret.d\nls -ld /home/victim/secret.d # =\u003e drwxrwxrwx (outside dir, attacker-chosen mode)\n```\nA single-run PoC is achievable against directory-based object sources (drive/onedrive-class) that satisfy both `ReadDirMetadata` and `CanHaveEmptyDirectories` and can present `pwn.rclonelink` and `pwn/` simultaneously. Local\u2192local uses the two-run backup model (same repeated-backup model as CVE-2024-52522 and CVE-2026-54572). Verified end-to-end against the real `fs/sync.Sync` engine on HEAD: the two-run backup backdated the outside target\u0027s mtime and chmod\u0027d it 0777 while os.Root correctly blocked the content-copy of `pwn/keep` \u2014 isolating the metadata gap.\n\n## Attack Chain\n1. **Entry.** Victim runs `rclone copy`/`sync --links [--metadata] \u003cuntrusted-remote\u003e: /dest`. Attacker controls source contents.\n - Guard: none \u2014 `--links` copying an untrusted remote is a documented, supported operation.\n2. **Plant symlink.** Source serves `pwn.rclonelink` with body = absolute outside path; rclone recreates `dst/pwn` \u2192 outside.\n - Guard: `Fs.symlink` routes creation through `os.Root.Symlink` (local.go:~1552).\n - Bypass proof: `os.Root` creates the link **verbatim by design** (commit 1154afe); the upstream `os.Root` fix\u0027s test `TestSymlinkEscapeWriteThroughBlocked` confirms only write-*through* is refused, the link is planted.\n3. **Deferred dir-metadata fires after transfers.** Source presents non-empty dir `pwn`; `setDelayedDirModTimes` (sync.go:1002) runs strictly after `stopTransfers()` (sync.go:988) \u2014 after the symlink is planted.\n - Guard: `MkdirMetadata` would create a real dir via os.Root-guarded `f.Mkdir` (local.go:897) inside its `errors.Is(err, os.ErrNotExist)` branch.\n - Bypass proof: `os.Lstat` (local.go:465) on the existing symlink returns success, so the `ErrNotExist` branch (local.go:896) is NOT taken; `f.Mkdir`/os.Root never runs. Empirically `os.IsNotExist(err)=false` for the planted symlink.\n4. **Sink follows the symlink.** `CopyDirMetadata`\u2192`MkdirMetadata`\u2192`writeMetadataToFile` runs `os.Chown`/`os.Chmod` (metadata.go:131/158); `DirSetModTime`\u2192`setTimes` runs `os.Chtimes` (local.go:1318) \u2014 all on `o.path=\"dst/pwn\"` with `translatedLink=false`.\n - Guard: CVE-2024-52522 NOFOLLOW branch (`os.Lchown`/`lChmod`/`lChtimes`).\n - Bypass proof: that branch is gated on `if o.translatedLink` (metadata.go:128/150, local.go:1315); a `Directory` always has `translatedLink=false`, so the raw following branch runs. POSIX-confirmed: `chmod 777`/`touch` on a symlink path change the *target\u0027s* mode/mtime.\n5. **Impact.** `chmod`/`chown`/`chtimes` on an attacker-chosen path outside the destination, with attacker-controlled values.\n\n## Bypass Evidence\n- `if o.translatedLink` gates verified verbatim on v1.75.0 at metadata.go:128/150 and local.go:1315; `os.Chown`/`os.Chmod`/`os.Chtimes` on the else branch at metadata.go:131/158 and local.go:1318.\n- `newDirectory`\u2192`newObject` (local.go:581/589/596) never sets the `.rclonelink` suffix \u2192 `translatedLink=false` for all directories.\n- `MkdirMetadata` skip branch: `os.Lstat` succeeds on planted symlink \u2192 `errors.Is(err, os.ErrNotExist)` false at local.go:896 \u2192 guarded `f.Mkdir` skipped.\n- Real `fs/sync.Sync` E2E on HEAD: `TestDirMetadataThroughPlantedSymlink` (outside dir \u2192 0777), `TestDirSetModTimeThroughPlantedSymlink` (mtime set, default-on), `TestE2E_TwoRunBackup` (backdated outside target while content-copy blocked by os.Root). All PASS. Control `TestControl_ContentWriteBlocked` confirms harness fidelity.\n\n## Affected Versions\n`\u003c= 1.75.0`. Vulnerable code present on latest release tag v1.75.0 and HEAD (5629f26); `git log v1.75.0..HEAD -- backend/local/metadata.go backend/local/local.go` is empty (no post-release fix).\n\n## Suggested Fix\nRoute directory metadata through `os.Root` when `TranslateSymlinks` is set (use `fchmodat(AT_SYMLINK_NOFOLLOW)`/`Lchown`/`UtimesNanoAt(AT_SYMLINK_NOFOLLOW)` on the `rel` path within the root), and/or extend `MkdirMetadata` to detect that the pre-existing destination path is a symlink and refuse to apply following-metadata \u2014 mirroring the CVE-2024-52522 NOFOLLOW branch that currently exists only for `translatedLink` objects.\n\n---\nReported by **zx (Jace)** \u2014 GitHub: @manus-use",
"id": "GHSA-f8g7-2xjc-7mfh",
"modified": "2026-09-10T22:51:34Z",
"published": "2026-09-10T22:51:34Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/security/advisories/GHSA-f8g7-2xjc-7mfh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-88016"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/commit/17b0c03338a857bcb0a68d2d4c82ddbdec3f7893"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/commit/a7ab39d3d1958afa1446982c1dc4e4a73a887e3e"
},
{
"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:L/UI:R/S:C/C:N/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "rclone: Directory metadata (chmod/chown/chtimes) applied through a planted symlink in rclone local --links escapes the destination"
}
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.
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.