CVE-2026-88016
ADVISORY - githubSummary
Summary
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.
- Guard: none —
- 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.
- Guard:
- 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.
- Guard:
- 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.
- Guard: CVE-2024-52522 NOFOLLOW branch (
- 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
NIST
CVSS SCORE
7.1highGitHub
CVSS SCORE
6.5mediumUbuntu
-