mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: harden doctor PATH checks and attribute cloud events by OS user
Doctor now verifies every installed package manager against the shim directory, and system-install validation only requires a safe parent directory. Cloud sync records username/uid on invocation context for multi-user hosts sharing one endpoint. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
controltowerv1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/controltower/v1"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
type cloudSink struct {
|
||||
*SyncClientBundle
|
||||
invocationID string
|
||||
ciResolver CloudSinkCIResolver
|
||||
ciResolver CloudSinkCIResolver
|
||||
command string
|
||||
workingDir string
|
||||
}
|
||||
@@ -94,6 +95,12 @@ func (s *cloudSink) buildInvocationContext() *controltowerv1.EndpointInvocationC
|
||||
ctx.SetCommand(s.command)
|
||||
ctx.SetWorkingDirectory(s.workingDir)
|
||||
|
||||
u, err := user.Current()
|
||||
if err == nil {
|
||||
ctx.SetUsername(u.Username)
|
||||
ctx.SetUsernameUid(u.Uid)
|
||||
}
|
||||
|
||||
if s.ciResolver != nil {
|
||||
ci := &controltowerv1.EndpointCIContext{}
|
||||
ci.SetProvider(s.ciResolver.Provider())
|
||||
|
||||
@@ -154,4 +154,6 @@ func TestCloudSinkSetsInvocationContextOnSessionComplete(t *testing.T) {
|
||||
require.NotNil(t, invCtx, "session complete event must have invocation context")
|
||||
assert.Contains(t, invCtx.GetCommand(), "npm")
|
||||
assert.NotEmpty(t, invCtx.GetWorkingDirectory())
|
||||
assert.NotEmpty(t, invCtx.GetUsername())
|
||||
assert.NotEmpty(t, invCtx.GetUsernameUid())
|
||||
}
|
||||
|
||||
+35
-28
@@ -21,7 +21,7 @@ var (
|
||||
systemBinDirOverride string
|
||||
systemProfilePathOverride string
|
||||
// systemExecutableOwnershipCheck requires root ownership of the binary and
|
||||
// its parent directories. Disabled in tests that cannot create root-owned files.
|
||||
// its parent directory. Disabled in tests that cannot create root-owned files.
|
||||
systemExecutableOwnershipCheck = true
|
||||
)
|
||||
|
||||
@@ -80,9 +80,9 @@ func newSystemShimManager(validateExecutable bool) (*ShimManager, error) {
|
||||
}
|
||||
|
||||
// validateSystemExecutable rejects binaries unsafe for system-wide shims.
|
||||
// System shims hard-code this path, so it must be world-executable, not
|
||||
// group/other-writable, and (when ownership checks are enabled) root-owned
|
||||
// under a root-owned, non-group/other-writable directory chain.
|
||||
// Shims hard-code this path, so the binary must be executable by all users,
|
||||
// not writable by group/others, and owned by root in a root-owned parent
|
||||
// directory that is not writable by group/others.
|
||||
func validateSystemExecutable(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
@@ -90,18 +90,25 @@ func validateSystemExecutable(path string) error {
|
||||
}
|
||||
|
||||
perm := info.Mode().Perm()
|
||||
if perm&0o001 == 0 {
|
||||
|
||||
// Other users must be able to exec the hard-coded pmg path from system shims.
|
||||
otherExecute := os.FileMode(0o001)
|
||||
// Group/other write would let another account replace the binary.
|
||||
groupOrOtherWrite := os.FileMode(0o022)
|
||||
|
||||
if perm&otherExecute == 0 {
|
||||
return fmt.Errorf("pmg executable %s is not executable by all users", path)
|
||||
}
|
||||
if perm&0o022 != 0 {
|
||||
if perm&groupOrOtherWrite != 0 {
|
||||
return fmt.Errorf("pmg executable %s is writable by group or others", path)
|
||||
}
|
||||
|
||||
if systemExecutableOwnershipCheck {
|
||||
// Root ownership of the binary and its parent blocks non-root replacement.
|
||||
if err := requireRootOwnedPath(path, info); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := requireSafeAncestorDirs(filepath.Dir(path)); err != nil {
|
||||
if err := requireSafeParentDir(filepath.Dir(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -119,28 +126,28 @@ func requireRootOwnedPath(path string, info os.FileInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireSafeAncestorDirs(dir string) error {
|
||||
for {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect directory %s: %w", dir, err)
|
||||
}
|
||||
if info.Mode().Perm()&0o022 != 0 {
|
||||
return fmt.Errorf("directory %s on pmg executable path is writable by group or others", dir)
|
||||
}
|
||||
uid, ok := fileOwnerUID(info)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot determine owner of directory %s", dir)
|
||||
}
|
||||
if uid != 0 {
|
||||
return fmt.Errorf("directory %s on pmg executable path must be owned by root", dir)
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return nil
|
||||
}
|
||||
dir = parent
|
||||
func requireSafeParentDir(dir string) error {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect directory %s: %w", dir, err)
|
||||
}
|
||||
|
||||
groupOrOtherWrite := os.FileMode(0o022)
|
||||
|
||||
if info.Mode().Perm()&groupOrOtherWrite != 0 {
|
||||
return fmt.Errorf("directory %s containing pmg executable is writable by group or others", dir)
|
||||
}
|
||||
|
||||
uid, ok := fileOwnerUID(info)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot determine owner of directory %s", dir)
|
||||
}
|
||||
|
||||
if uid != 0 {
|
||||
return fmt.Errorf("directory %s containing pmg executable must be owned by root", dir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SystemShimsInstalled reports whether the system shim directory contains at
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ func PrintSetupSystemInstallCmdInfo(shimBinDir, configDir, profilePath string) {
|
||||
fmt.Printf(" %s\n", Colors.Dim(fmt.Sprintf("Shims: %s", shimBinDir)))
|
||||
fmt.Printf(" %s\n", Colors.Dim(fmt.Sprintf("Config: %s", configDir)))
|
||||
fmt.Printf(" %s\n", Colors.Dim(fmt.Sprintf("Profile: %s", profilePath)))
|
||||
fmt.Printf(" %s\n", Colors.Dim(fmt.Sprintf("Per-user config files are now ignored; edit %s/config.yml as root.", configDir)))
|
||||
fmt.Printf(" %s\n", Colors.Dim("Per-user config files are now ignored."))
|
||||
fmt.Printf("\n%s For Docker builds (RUN does not source profile.d), add:\n", Colors.Dim("ℹ"))
|
||||
fmt.Printf(" %s\n", Colors.Bold(fmt.Sprintf(`ENV PATH="%s:$PATH"`, shimBinDir)))
|
||||
fmt.Printf("%s Login shells pick up PATH from profile.d. After venv activate, use `pmg pip`.\n", Colors.Dim("ℹ"))
|
||||
|
||||
Reference in New Issue
Block a user