From faaf716e9b8f64bdb3203e021eac690e5e9a1aec Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sat, 25 Jul 2026 11:17:27 +0100 Subject: [PATCH] rc: don't expose pprof debug handlers on an unauthenticated server GHSA-mfvx-7rcj-9m5g CVE-PENDING The pprof debug handlers were accessible without authentication disclosing the process command line (which can carry backend credentials passed on the command line) and runtime profiles. Mount the pprof handlers only when when auth is configured or --rc-no-auth was passed - so they obey the same rule as the rc endpoints. Addresses GHSA-mfvx-7rcj-9m5g finding 1. --- docs/content/rc.md | 8 ++++++ fs/rc/rcserver/rcserver.go | 7 +++-- fs/rc/rcserver/rcserver_test.go | 51 +++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 10 deletions(-) diff --git a/docs/content/rc.md b/docs/content/rc.md index 49cb5e58f..ea3d0a00b 100644 --- a/docs/content/rc.md +++ b/docs/content/rc.md @@ -2742,6 +2742,14 @@ curl -H "Content-Type: application/json" -X POST -d '{"potato":2,"sausage":1}' ' If you use the `--rc` flag this will also enable the use of the go profiling tools on the same port. +The profiling endpoints follow the same authentication rules as the +rest of the rc: they are only served if authentication has been set up +(`--rc-user`/`--rc-pass` or `--rc-htpasswd`) or the +[`--rc-no-auth`](#--rc-no-auth) flag is in use. For debugging on the +default localhost port the easiest thing is to use `--rc --rc-no-auth` +(but see the warning about using `--rc-no-auth` on a non-loopback +bind). + To use these, first [install go](https://golang.org/doc/install). ### Debugging memory use diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go index 37b3c587f..c32aa0ea6 100644 --- a/fs/rc/rcserver/rcserver.go +++ b/fs/rc/rcserver/rcserver.go @@ -126,8 +126,11 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) (*Serve middleware.SetHeader("Server", "rclone/"+fs.Version), ) - // Add the debug handler which is installed in the default mux - router.Handle("/debug/pprof/*", mux) + // Add the debug handler which is installed in the default mux. + // Only do this if auth is enabled. + if s.noAuth || s.server.UsingAuth() { + router.Handle("/debug/pprof/*", mux) + } // FIXME split these up into individual functions router.Get("/*", s.handler) diff --git a/fs/rc/rcserver/rcserver_test.go b/fs/rc/rcserver/rcserver_test.go index 98bc895c6..11cacf964 100644 --- a/fs/rc/rcserver/rcserver_test.go +++ b/fs/rc/rcserver/rcserver_test.go @@ -977,27 +977,62 @@ func TestRCAsync(t *testing.T) { testServer(t, tests, &opt) } -// Check the debug handlers are attached +// The debug/pprof handlers expose the process command line, heap and +// goroutine dumps, so require auth. func TestRCDebug(t *testing.T) { - tests := []testRun{{ + index := testRun{ Name: "index", URL: "debug/pprof/", Method: "GET", ContentType: "text/html", Status: http.StatusOK, Contains: regexp.MustCompile(`Types of profiles available`), - }, { + } + goroutines := testRun{ Name: "goroutines", URL: "debug/pprof/goroutine?debug=1", Method: "GET", ContentType: "text/html", Status: http.StatusOK, Contains: regexp.MustCompile(`goroutine profile`), - }} - opt := newTestOpt() - opt.Serve = true - opt.Files = "" - testServer(t, tests, &opt) + } + + // With --rc-no-auth the debug handlers are attached and open + t.Run("NoAuth", func(t *testing.T) { + opt := newTestOpt() + opt.Serve = true + opt.Files = "" + opt.NoAuth = true + testServer(t, []testRun{index, goroutines}, &opt) + }) + + // With auth configured the debug handlers are attached but require credentials + t.Run("WithAuth", func(t *testing.T) { + authed := index + authed.User, authed.Pass = "user", "pass" + unauthed := index + unauthed.Name = "index-no-credentials" + unauthed.Status = http.StatusUnauthorized + unauthed.Contains = regexp.MustCompile(`Unauthorized`) + opt := newTestOpt() + opt.Serve = true + opt.Files = "" + opt.Auth.BasicUser = "user" + opt.Auth.BasicPass = "pass" + testServer(t, []testRun{authed, unauthed}, &opt) + }) + + // On a default unauthenticated server the debug handlers must not be + // mounted, so they bypass nothing - they simply aren't there + t.Run("FailClosed", func(t *testing.T) { + blocked := index + blocked.Status = http.StatusNotFound + blocked.Contains = regexp.MustCompile(`Not Found`) + opt := newTestOpt() + opt.Serve = true + opt.Files = "" + testServer(t, []testRun{blocked}, &opt) + }) } func TestServeModTime(t *testing.T) {