mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
fix: harden system-install review findings
Require root-owned, non-group/other-writable pmg for --system install; allow remove without that validation. Doctor checks npm resolution for PATH precedence, uses ImpliesInterception instead of message matching, and documents version-manager shadowing. Pass profile bin dir from the shim manager and note that system config ignores per-user files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
//go:build unix
|
||||
|
||||
package shim
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func fileOwnerUID(info os.FileInfo) (uint32, bool) {
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return uint32(stat.Uid), true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build windows
|
||||
|
||||
package shim
|
||||
|
||||
import "os"
|
||||
|
||||
func fileOwnerUID(info os.FileInfo) (uint32, bool) {
|
||||
return 0, false
|
||||
}
|
||||
@@ -82,7 +82,7 @@ func (m *ShimManager) Install() error {
|
||||
}
|
||||
|
||||
if m.config.ManageProfile {
|
||||
if err := writeSystemProfile(); err != nil {
|
||||
if err := writeSystemProfile(m.config.BinDir); err != nil {
|
||||
return fmt.Errorf("failed to write system profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
+73
-6
@@ -20,6 +20,9 @@ const (
|
||||
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.
|
||||
systemExecutableOwnershipCheck = true
|
||||
)
|
||||
|
||||
// SystemBinDir returns the directory for system-wide PMG shims.
|
||||
@@ -40,15 +43,29 @@ func SystemProfilePath() string {
|
||||
|
||||
// NewSystemShimManager creates a shim manager for system-wide install: shims
|
||||
// under SystemBinDir, no per-user rc edits, and /etc/profile.d management.
|
||||
// The current executable is validated for multi-user use.
|
||||
func NewSystemShimManager() (*ShimManager, error) {
|
||||
return newSystemShimManager(true)
|
||||
}
|
||||
|
||||
// NewSystemShimManagerForRemove creates a system shim manager without
|
||||
// validating the current executable. Uninstall must work even when the binary
|
||||
// that originally installed the shims is no longer suitable for install.
|
||||
func NewSystemShimManagerForRemove() (*ShimManager, error) {
|
||||
return newSystemShimManager(false)
|
||||
}
|
||||
|
||||
func newSystemShimManager(validateExecutable bool) (*ShimManager, error) {
|
||||
aliasCfg := alias.DefaultConfig()
|
||||
pmgBin, err := currentExecutable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateSystemExecutable(pmgBin); err != nil {
|
||||
return nil, err
|
||||
if validateExecutable {
|
||||
if err := validateSystemExecutable(pmgBin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &ShimManager{
|
||||
@@ -62,19 +79,70 @@ func NewSystemShimManager() (*ShimManager, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// validateSystemExecutable rejects binaries other users cannot execute. System shims hard-code this path.
|
||||
// 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.
|
||||
func validateSystemExecutable(path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect pmg executable %s: %w", path, err)
|
||||
}
|
||||
|
||||
if info.Mode().Perm()&0o001 == 0 {
|
||||
perm := info.Mode().Perm()
|
||||
if perm&0o001 == 0 {
|
||||
return fmt.Errorf("pmg executable %s is not executable by all users", path)
|
||||
}
|
||||
if perm&0o022 != 0 {
|
||||
return fmt.Errorf("pmg executable %s is writable by group or others", path)
|
||||
}
|
||||
|
||||
if systemExecutableOwnershipCheck {
|
||||
if err := requireRootOwnedPath(path, info); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := requireSafeAncestorDirs(filepath.Dir(path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireRootOwnedPath(path string, info os.FileInfo) error {
|
||||
uid, ok := fileOwnerUID(info)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot determine owner of %s", path)
|
||||
}
|
||||
if uid != 0 {
|
||||
return fmt.Errorf("pmg executable %s must be owned by root", path)
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// SystemShimsInstalled reports whether the system shim directory contains at
|
||||
// least one shim script.
|
||||
func SystemShimsInstalled() bool {
|
||||
@@ -108,8 +176,7 @@ func SystemProfileInstalled() bool {
|
||||
return strings.Contains(string(data), systemProfileMarker)
|
||||
}
|
||||
|
||||
func writeSystemProfile() error {
|
||||
binDir := SystemBinDir()
|
||||
func writeSystemProfile(binDir string) error {
|
||||
path := SystemProfilePath()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
|
||||
@@ -13,9 +13,11 @@ func useSystemPaths(t *testing.T, dir string) {
|
||||
t.Helper()
|
||||
systemBinDirOverride = filepath.Join(dir, "bin")
|
||||
systemProfilePathOverride = filepath.Join(dir, "profile.d", "pmg.sh")
|
||||
systemExecutableOwnershipCheck = false
|
||||
t.Cleanup(func() {
|
||||
systemBinDirOverride = ""
|
||||
systemProfilePathOverride = ""
|
||||
systemExecutableOwnershipCheck = true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -100,15 +102,20 @@ func TestWriteSystemProfileRepairsStalePath(t *testing.T) {
|
||||
0o644,
|
||||
))
|
||||
|
||||
require.NoError(t, writeSystemProfile())
|
||||
binDir := filepath.Join(root, "custom-bin")
|
||||
require.NoError(t, writeSystemProfile(binDir))
|
||||
|
||||
content, err := os.ReadFile(SystemProfilePath())
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(content), SystemBinDir())
|
||||
assert.Contains(t, string(content), binDir)
|
||||
assert.NotContains(t, string(content), "/stale/path")
|
||||
assert.NotContains(t, string(content), SystemBinDir())
|
||||
}
|
||||
|
||||
func TestValidateSystemExecutableRejectsPrivateBinary(t *testing.T) {
|
||||
systemExecutableOwnershipCheck = false
|
||||
t.Cleanup(func() { systemExecutableOwnershipCheck = true })
|
||||
|
||||
privateDir := t.TempDir()
|
||||
privateExecutable := filepath.Join(privateDir, "pmg")
|
||||
require.NoError(t, os.WriteFile(privateExecutable, []byte("binary"), 0o700))
|
||||
@@ -118,3 +125,40 @@ func TestValidateSystemExecutableRejectsPrivateBinary(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not executable by all users")
|
||||
}
|
||||
|
||||
func TestValidateSystemExecutableRejectsGroupWritable(t *testing.T) {
|
||||
systemExecutableOwnershipCheck = false
|
||||
t.Cleanup(func() { systemExecutableOwnershipCheck = true })
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pmg")
|
||||
require.NoError(t, os.WriteFile(path, []byte("binary"), 0o755))
|
||||
require.NoError(t, os.Chmod(path, 0o775))
|
||||
|
||||
err := validateSystemExecutable(path)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "writable by group or others")
|
||||
}
|
||||
|
||||
func TestValidateSystemExecutableRejectsNonRootOwner(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "pmg")
|
||||
require.NoError(t, os.WriteFile(path, []byte("binary"), 0o755))
|
||||
|
||||
err := validateSystemExecutable(path)
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "must be owned by root")
|
||||
}
|
||||
|
||||
func TestNewSystemShimManagerForRemoveSkipsValidation(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
useSystemPaths(t, root)
|
||||
systemExecutableOwnershipCheck = true
|
||||
|
||||
mgr, err := NewSystemShimManagerForRemove()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mgr.Install())
|
||||
require.NoError(t, mgr.Remove())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user