mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: gate SUDO_USER trust and root path diversion; add doctor binary check
Address review findings on the system-install PR: - cloud_sink: honor SUDO_USER for audit attribution only when euid==0. Without the gate any user could set SUDO_USER and spoof cloud-audit attribution to another account. Matches the guard in cmd/setup/cert.go. - config: divert per-user paths to root's passwd home only on an actual sudo elevation (euid==0 && SUDO_USER set), not for every root euid. The blanket root diversion ignored HOME/XDG_CONFIG_HOME and silently stopped reading genuine root users' config (golden Docker images), regressing two tests that only fail when the suite runs as root. Genuine root honors the environment as before; su without - leaves no marker and stays a documented, loud-failing residual. - doctor: add a system-only check re-validating that the binary the installed shims exec is still root-owned and non-writable, catching permission/ownership drift after install. - shim: fold the duplicated shim-scan loop into firstShimContent.
This commit is contained in:
+15
-2
@@ -705,6 +705,19 @@ func pathWithinDir(path, dir string) bool {
|
||||
return cleanPath == cleanDir || strings.HasPrefix(cleanPath, cleanDir+string(os.PathSeparator))
|
||||
}
|
||||
|
||||
// isSudoElevation reports whether pmg is running as root via sudo, i.e. a
|
||||
// non-root user elevated and sudo may have preserved that user's HOME/XDG_*.
|
||||
// Only then do per-user paths divert to root's own home, so root does not
|
||||
// create state inside the invoking user's home. Running genuinely as root
|
||||
// (no sudo) keeps honoring HOME/XDG_*, which is legitimate and intended (e.g.
|
||||
// golden Docker images that set HOME/XDG_CONFIG_HOME on purpose). This mirrors
|
||||
// the SUDO_USER guard used elsewhere (cmd/setup/cert.go). su without sudo does
|
||||
// not set SUDO_USER and is not covered; the unwritable-dir remedy still guides
|
||||
// the user if such a run poisons a directory.
|
||||
func isSudoElevation() bool {
|
||||
return configGeteuid() == 0 && os.Getenv("SUDO_USER") != ""
|
||||
}
|
||||
|
||||
// configDir computes the path to the config directory.
|
||||
func configDir() (string, error) {
|
||||
dir := os.Getenv(pmgConfigDirEnvKey)
|
||||
@@ -712,7 +725,7 @@ func configDir() (string, error) {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
if configGeteuid() == 0 {
|
||||
if isSudoElevation() {
|
||||
if base, err := rootConfigDirResolver(); err == nil {
|
||||
return filepath.Join(base, pmgDefaultHomeRelativePath), nil
|
||||
} else {
|
||||
@@ -844,7 +857,7 @@ func cacheDir() (string, error) {
|
||||
}
|
||||
return filepath.Join(baseDir, pmgDefaultHomeRelativePath), nil
|
||||
case "darwin", "linux":
|
||||
if configGeteuid() == 0 {
|
||||
if isSudoElevation() {
|
||||
if base, err := rootCacheDirResolver(); err == nil {
|
||||
return filepath.Join(base, pmgDefaultHomeRelativePath), nil
|
||||
} else {
|
||||
|
||||
+29
-3
@@ -25,9 +25,10 @@ func poisonUserEnv(t *testing.T) {
|
||||
t.Setenv("XDG_CACHE_HOME", "/home/victim/.cache")
|
||||
}
|
||||
|
||||
func TestConfigDirAsRootIgnoresPreservedHome(t *testing.T) {
|
||||
func TestConfigDirUnderSudoIgnoresPreservedHome(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "victim")
|
||||
|
||||
dir, err := configDir()
|
||||
require.NoError(t, err)
|
||||
@@ -38,6 +39,18 @@ func TestConfigDirAsRootIgnoresPreservedHome(t *testing.T) {
|
||||
assert.NotContains(t, dir, "/home/victim")
|
||||
}
|
||||
|
||||
func TestConfigDirGenuineRootHonorsEnv(t *testing.T) {
|
||||
// Root without sudo (SUDO_USER unset) is the intended user, e.g. a golden
|
||||
// Docker image that deliberately sets XDG_CONFIG_HOME. It must not divert.
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "")
|
||||
|
||||
dir, err := configDir()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, dir, "/home/victim")
|
||||
}
|
||||
|
||||
func TestConfigDirAsNonRootUsesEnvHome(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 1000)
|
||||
@@ -47,19 +60,21 @@ func TestConfigDirAsNonRootUsesEnvHome(t *testing.T) {
|
||||
assert.Contains(t, dir, "/home/victim")
|
||||
}
|
||||
|
||||
func TestConfigDirEnvOverrideWinsForRoot(t *testing.T) {
|
||||
func TestConfigDirEnvOverrideWinsUnderSudo(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
t.Setenv("PMG_CONFIG_DIR", "/custom/pmg")
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "victim")
|
||||
|
||||
dir, err := configDir()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "/custom/pmg", dir)
|
||||
}
|
||||
|
||||
func TestCacheDirAsRootIgnoresPreservedHome(t *testing.T) {
|
||||
func TestCacheDirUnderSudoIgnoresPreservedHome(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "victim")
|
||||
|
||||
dir, err := cacheDir()
|
||||
require.NoError(t, err)
|
||||
@@ -70,6 +85,16 @@ func TestCacheDirAsRootIgnoresPreservedHome(t *testing.T) {
|
||||
assert.NotContains(t, dir, "/home/victim")
|
||||
}
|
||||
|
||||
func TestCacheDirGenuineRootHonorsEnv(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "")
|
||||
|
||||
dir, err := cacheDir()
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, dir, "/home/victim")
|
||||
}
|
||||
|
||||
func TestCacheDirAsNonRootUsesEnvHome(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 1000)
|
||||
@@ -82,6 +107,7 @@ func TestCacheDirAsNonRootUsesEnvHome(t *testing.T) {
|
||||
func TestRootDirsFallBackToEnvWhenPasswdUnavailable(t *testing.T) {
|
||||
poisonUserEnv(t)
|
||||
withEuid(t, 0)
|
||||
t.Setenv("SUDO_USER", "victim")
|
||||
|
||||
origConfig, origCache := rootConfigDirResolver, rootCacheDirResolver
|
||||
rootConfigDirResolver = func() (string, error) { return "", assert.AnError }
|
||||
|
||||
Reference in New Issue
Block a user