From 4698ee762cf4967df1e21be24c13d355a4fe9aa2 Mon Sep 17 00:00:00 2001 From: Andrew Block Date: Mon, 22 Jun 2026 18:56:49 -0500 Subject: [PATCH] Fix issue when changing user in exec subcommands Signed-off-by: Andrew Block --- cmd/sops/subcommand/exec/exec.go | 6 ++ cmd/sops/subcommand/exec/exec_unix.go | 12 +++ cmd/sops/subcommand/exec/exec_unix_test.go | 115 +++++++++++++++++++++ cmd/sops/subcommand/exec/exec_windows.go | 4 + 4 files changed, 137 insertions(+) create mode 100644 cmd/sops/subcommand/exec/exec_unix_test.go diff --git a/cmd/sops/subcommand/exec/exec.go b/cmd/sops/subcommand/exec/exec.go index 35f519c3cc..0abb16c19b 100644 --- a/cmd/sops/subcommand/exec/exec.go +++ b/cmd/sops/subcommand/exec/exec.go @@ -57,7 +57,9 @@ func GetFile(dir, filename string) (*os.File, error) { } func ExecWithFile(opts ExecOpts) error { + var userEnv []string if opts.User != "" { + userEnv = UserEnv(opts.User) SwitchUser(opts.User) } @@ -107,6 +109,7 @@ func ExecWithFile(opts ExecOpts) error { if !opts.Pristine { env = os.Environ() } + env = append(env, userEnv...) env = append(env, opts.Env...) placeholdered := strings.Replace(opts.Command, "{}", filename, -1) @@ -125,7 +128,9 @@ func ExecWithFile(opts ExecOpts) error { } func ExecWithEnv(opts ExecOpts) error { + var userEnv []string if opts.User != "" { + userEnv = UserEnv(opts.User) SwitchUser(opts.User) } @@ -150,6 +155,7 @@ func ExecWithEnv(opts ExecOpts) error { env = append(env, string(line)) } + env = append(env, userEnv...) env = append(env, opts.Env...) if opts.SameProcess { diff --git a/cmd/sops/subcommand/exec/exec_unix.go b/cmd/sops/subcommand/exec/exec_unix.go index c17e4a109c..e31aa6d580 100644 --- a/cmd/sops/subcommand/exec/exec_unix.go +++ b/cmd/sops/subcommand/exec/exec_unix.go @@ -42,6 +42,18 @@ func GetPipe(dir, filename string) (string, error) { return tmpfn, nil } +func UserEnv(username string) []string { + u, err := user.Lookup(username) + if err != nil { + log.Fatal(err) + } + return []string{ + "HOME=" + u.HomeDir, + "USER=" + u.Username, + "LOGNAME=" + u.Username, + } +} + func SwitchUser(username string) { user, err := user.Lookup(username) if err != nil { diff --git a/cmd/sops/subcommand/exec/exec_unix_test.go b/cmd/sops/subcommand/exec/exec_unix_test.go new file mode 100644 index 0000000000..f94fb2a267 --- /dev/null +++ b/cmd/sops/subcommand/exec/exec_unix_test.go @@ -0,0 +1,115 @@ +//go:build !windows +// +build !windows + +package exec + +import ( + "os" + "os/user" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUserEnvReturnsCorrectVars(t *testing.T) { + currentUser, err := user.Current() + require.NoError(t, err) + + env := UserEnv(currentUser.Username) + + assert.Contains(t, env, "HOME="+currentUser.HomeDir) + assert.Contains(t, env, "USER="+currentUser.Username) + assert.Contains(t, env, "LOGNAME="+currentUser.Username) + assert.Len(t, env, 3) +} + +func TestUserEnvDoesNotModifyProcessEnv(t *testing.T) { + currentUser, err := user.Current() + require.NoError(t, err) + + originalHome := os.Getenv("HOME") + + UserEnv(currentUser.Username) + + assert.Equal(t, originalHome, os.Getenv("HOME"), + "HOME should not be modified in the current process") +} + +func TestExecWithFilePassesUserEnvToChild(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root privileges") + } + + currentUser, err := user.Current() + require.NoError(t, err) + + originalHome := os.Getenv("HOME") + + err = ExecWithFile(ExecOpts{ + Command: "env | grep ^HOME=", + Plaintext: []byte("hello"), + User: currentUser.Username, + Fifo: false, + }) + require.NoError(t, err) + + assert.Equal(t, originalHome, os.Getenv("HOME"), + "ExecWithFile should not modify HOME in the current process") +} + +func TestExecWithEnvPassesUserEnvToChild(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root privileges") + } + + currentUser, err := user.Current() + require.NoError(t, err) + + originalHome := os.Getenv("HOME") + + err = ExecWithEnv(ExecOpts{ + Command: "true", + Plaintext: []byte{}, + User: currentUser.Username, + }) + require.NoError(t, err) + + assert.Equal(t, originalHome, os.Getenv("HOME"), + "ExecWithEnv should not modify HOME in the current process") +} + +func TestExecWithFilePristineIncludesUserEnv(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root privileges") + } + + currentUser, err := user.Current() + require.NoError(t, err) + + err = ExecWithFile(ExecOpts{ + Command: "env | grep -q ^HOME=", + Plaintext: []byte("hello"), + User: currentUser.Username, + Pristine: true, + Fifo: false, + }) + require.NoError(t, err, "child should have HOME even with --pristine when --user is set") +} + +func TestExecWithEnvPristineIncludesUserEnv(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root privileges") + } + + currentUser, err := user.Current() + require.NoError(t, err) + + err = ExecWithEnv(ExecOpts{ + Command: "env | grep -q ^HOME=", + Plaintext: []byte{}, + User: currentUser.Username, + Pristine: true, + }) + require.NoError(t, err, "child should have HOME even with --pristine when --user is set") +} diff --git a/cmd/sops/subcommand/exec/exec_windows.go b/cmd/sops/subcommand/exec/exec_windows.go index c4870ba62d..3fb61ecc89 100644 --- a/cmd/sops/subcommand/exec/exec_windows.go +++ b/cmd/sops/subcommand/exec/exec_windows.go @@ -22,6 +22,10 @@ func GetPipe(dir, filename string) (string, error) { return "", fmt.Errorf("fifos are not available on windows") } +func UserEnv(username string) []string { + return nil +} + func SwitchUser(username string) { log.Fatal("user switching not available on windows") }