From dade21c1616035b044df0eef7ee6a85aeb06a139 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 23 Jun 2026 14:33:45 +0100 Subject: [PATCH] serve restic: fix --private-repos isolation bypass CVE-2026-59733 A user could reach another user's private repository by sending a path such as //..//config. The authorization check compares the first path segment against the authenticated user, while the backend object key was built from the raw, un-cleaned URL path. Reject any non-canonical request path so the authorization segment and the backend object key can no longer disagree. Fixes GHSA-fqj9-69pf-6pjg --- cmd/serve/restic/restic.go | 6 ++++++ cmd/serve/restic/restic_privaterepos_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/cmd/serve/restic/restic.go b/cmd/serve/restic/restic.go index 24af0db69..8c173a436 100644 --- a/cmd/serve/restic/restic.go +++ b/cmd/serve/restic/restic.go @@ -244,6 +244,12 @@ func WithRemote(next http.Handler) http.Handler { urlpath = r.URL.Path } urlpath = strings.Trim(urlpath, "/") + // Reject any non-canonical path, in particular one containing ".." + // traversal elements. + if urlpath != "" && path.Clean(urlpath) != urlpath { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } parts := matchData.FindStringSubmatch(urlpath) // if no data directory, layout is flat if parts != nil { diff --git a/cmd/serve/restic/restic_privaterepos_test.go b/cmd/serve/restic/restic_privaterepos_test.go index 21e1d68ce..e59d9baf6 100644 --- a/cmd/serve/restic/restic_privaterepos_test.go +++ b/cmd/serve/restic/restic_privaterepos_test.go @@ -75,4 +75,16 @@ func TestResticPrivateRepositories(t *testing.T) { checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusForbidden)}) } + // Reaching another user's repo via a ".." traversal in the path must be + // rejected even though the {userID} segment matches the authenticated user + // (GHSA-fqj9-69pf-6pjg). + reqs = []*http.Request{ + newAuthenticatedRequest(t, "GET", "/test/../other_user/config", nil, opt.Auth.BasicUser, opt.Auth.BasicPass), + newAuthenticatedRequest(t, "POST", "/test/../other_user/config", strings.NewReader("evil"), opt.Auth.BasicUser, opt.Auth.BasicPass), + newAuthenticatedRequest(t, "DELETE", "/test/../other_user/config", nil, opt.Auth.BasicUser, opt.Auth.BasicPass), + } + for _, req := range reqs { + checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusBadRequest)}) + } + }