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 /<me>/../<victim>/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
This commit is contained in:
Nick Craig-Wood
2026-06-23 14:33:45 +01:00
parent d11efe0d58
commit dade21c161
2 changed files with 18 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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)})
}
}