rc: stop global.* connection string options changing config CVE-2026-49980
A connection string can carry global.* options which change rclone's process-wide configuration (e.g. global.http_proxy). This is undesirable for the rc interface which was designed to have multiple users or connections at once. The rc interface has the `_config` mechanism for setting request scoped global config. This blocks global.* options on all rc paths by marking the context as a remote control request at the rc boundaries. fs.NewFs then skips applying global.* to the process-wide config for a marked context. The marker is reapplied in fs.CopyConfig, which is the call rclone uses to detach context but keep config. global.* options still apply to the individual backend they are set on, exactly like override.* options; they just no longer leak into the rest of the process. Remotes created directly on the command line are unaffected as are remotes defined in the config file. See: GHSA-qw24-gh76-8rvv
This commit is contained in:
@@ -328,6 +328,9 @@ func (jobs *Jobs) NewJob(ctx context.Context, fn rc.Func, in rc.Params) (job *Jo
|
||||
// Add the job to the context
|
||||
ctx = context.WithValue(ctx, jobKey, job)
|
||||
|
||||
// Mark the context as created from an rc request
|
||||
ctx = fs.WithRCRequest(ctx)
|
||||
|
||||
if isAsync {
|
||||
go job.run(ctx, fn, in)
|
||||
out = make(rc.Params)
|
||||
|
||||
@@ -300,6 +300,37 @@ func TestExecuteJobWithConfig(t *testing.T) {
|
||||
assert.NotEqual(t, 42*fs.Mebi, ci.BufferSize)
|
||||
}
|
||||
|
||||
// NewJob must mark the context as a remote control (rc) request, including for
|
||||
// asynchronous jobs whose context is detached
|
||||
func TestNewJobMarksRCRequest(t *testing.T) {
|
||||
jobID.Store(0)
|
||||
jobs := newJobs() // local instance so we don't pollute the global registry
|
||||
|
||||
// synchronous job
|
||||
var syncMarked bool
|
||||
_, _, err := jobs.NewJob(context.Background(), func(ctx context.Context, in rc.Params) (rc.Params, error) {
|
||||
syncMarked = fs.IsRCRequest(ctx)
|
||||
return nil, nil
|
||||
}, rc.Params{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, syncMarked, "sync rc job context must be marked as an rc request")
|
||||
|
||||
// asynchronous job - the context is detached, so the marker must be set
|
||||
// after that detachment to survive
|
||||
done := make(chan bool, 1)
|
||||
_, _, err = jobs.NewJob(context.Background(), func(ctx context.Context, in rc.Params) (rc.Params, error) {
|
||||
done <- fs.IsRCRequest(ctx)
|
||||
return nil, nil
|
||||
}, rc.Params{"_async": true})
|
||||
require.NoError(t, err)
|
||||
select {
|
||||
case asyncMarked := <-done:
|
||||
assert.True(t, asyncMarked, "async rc job context must be marked as an rc request")
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("async job did not run")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteJobWithFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
called := false
|
||||
|
||||
@@ -348,7 +348,7 @@ func (s *Server) serveRoot(w http.ResponseWriter, r *http.Request) {
|
||||
// Instantiating a backend from request-supplied configuration can execute
|
||||
// commands during initialisation (e.g. webdav bearer_token_command, sftp ssh),
|
||||
// read arbitrary local files, or mutate process-wide config via global.*
|
||||
// options. See GHSA-qw24-gh76-8rvv.
|
||||
// options so shouldn't be done without authentication.
|
||||
//
|
||||
// authenticated must be true if the request has been authenticated (HTTP auth
|
||||
// is configured on the server) or --rc-no-auth was passed to explicitly opt in
|
||||
@@ -391,7 +391,8 @@ func (s *Server) serveRemote(w http.ResponseWriter, r *http.Request, path string
|
||||
writeError(path, nil, w, err, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
f, err := cache.Get(s.ctx, fsName)
|
||||
// Mark as an rc request e.g. so NewFs can reject global.* config
|
||||
f, err := cache.Get(fs.WithRCRequest(s.ctx), fsName)
|
||||
if err != nil {
|
||||
writeError(path, nil, w, fmt.Errorf("failed to make Fs: %w", err), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -480,6 +480,30 @@ func TestServeRemoteWithAuth(t *testing.T) {
|
||||
testServer(t, tests, &opt)
|
||||
}
|
||||
|
||||
// The serve path must mark backend creation as a remote control (rc) request
|
||||
// so a request-supplied remote can't change process-wide config via global.*
|
||||
// options.
|
||||
func TestServeRemoteMarksRCRequest(t *testing.T) {
|
||||
configfile.Install()
|
||||
opt := newTestOpt()
|
||||
opt.Serve = true
|
||||
opt.NoAuth = true // allow inline remotes so we reach backend creation
|
||||
opt.Template.Path = defaultTestTemplate
|
||||
rcServer, err := newServer(context.Background(), &opt, http.NewServeMux())
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
original := fs.GetConfig(ctx).UserAgent
|
||||
defer func() { fs.GetConfig(ctx).UserAgent = original }()
|
||||
|
||||
// serveRemote uses s.ctx, marked as an rc request, so global.* must not
|
||||
// change the process-wide config.
|
||||
rcServer.serveRemote(httptest.NewRecorder(), httptest.NewRequest("GET", "/file.txt", nil),
|
||||
"file.txt", ":local,global.user_agent=rcservertest:"+testFs)
|
||||
assert.Equal(t, original, fs.GetConfig(ctx).UserAgent,
|
||||
"the serve path must not let global.* change the process-wide config")
|
||||
}
|
||||
|
||||
func TestRC(t *testing.T) {
|
||||
tests := []testRun{{
|
||||
Name: "rc-root",
|
||||
|
||||
Reference in New Issue
Block a user