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:
+81
-32
@@ -3,7 +3,9 @@ package setup
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/safedep/pmg/config"
|
||||
"github.com/safedep/pmg/internal/alias"
|
||||
@@ -139,8 +141,9 @@ func runCoreChecks(cfg *config.RuntimeConfig) []doctor.CheckResult {
|
||||
}
|
||||
if installed {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: aliasesInstalledMessage,
|
||||
Status: doctor.StatusPass,
|
||||
Message: aliasesInstalledMessage,
|
||||
ImpliesInterception: true,
|
||||
}
|
||||
}
|
||||
if shim.SystemShimsInstalled() {
|
||||
@@ -189,32 +192,7 @@ func runCoreChecks(cfg *config.RuntimeConfig) []doctor.CheckResult {
|
||||
{
|
||||
Name: checkShimInPath,
|
||||
Category: "Shell Integration",
|
||||
Run: func() doctor.CheckResult {
|
||||
pathEntries := filepath.SplitList(os.Getenv("PATH"))
|
||||
systemDir := shim.SystemBinDir()
|
||||
if shim.SystemShimsInstalled() && pathContainsDir(pathEntries, systemDir) {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "System shim directory is in PATH",
|
||||
}
|
||||
}
|
||||
if userDir, err := shim.UserBinDir(); err == nil && pathContainsDir(pathEntries, userDir) {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "Shim directory is in PATH",
|
||||
}
|
||||
}
|
||||
if shim.SystemShimsInstalled() {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "System shim directory not in PATH",
|
||||
}
|
||||
}
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Shim directory not in PATH",
|
||||
}
|
||||
},
|
||||
Run: checkShimInPathResult,
|
||||
},
|
||||
{
|
||||
Name: checkProxyMode,
|
||||
@@ -313,6 +291,80 @@ func pathContainsDir(pathEntries []string, dir string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func pathIsUnderDir(path, dir string) bool {
|
||||
if path == "" || dir == "" {
|
||||
return false
|
||||
}
|
||||
cleanPath := filepath.Clean(path)
|
||||
cleanDir := filepath.Clean(dir)
|
||||
if cleanPath == cleanDir {
|
||||
return true
|
||||
}
|
||||
prefix := cleanDir + string(os.PathSeparator)
|
||||
return strings.HasPrefix(cleanPath, prefix)
|
||||
}
|
||||
|
||||
func checkShimInPathResult() doctor.CheckResult {
|
||||
pathEntries := filepath.SplitList(os.Getenv("PATH"))
|
||||
systemDir := shim.SystemBinDir()
|
||||
userDir, userDirErr := shim.UserBinDir()
|
||||
resolved, lookErr := exec.LookPath("npm")
|
||||
|
||||
if lookErr == nil {
|
||||
if shim.SystemShimsInstalled() && pathIsUnderDir(resolved, systemDir) {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "npm resolves to system shim",
|
||||
ImpliesInterception: true,
|
||||
}
|
||||
}
|
||||
if userDirErr == nil && pathIsUnderDir(resolved, userDir) {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "npm resolves to PMG shim",
|
||||
ImpliesInterception: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shim.SystemShimsInstalled() && pathContainsDir(pathEntries, systemDir) {
|
||||
if lookErr == nil {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusWarn,
|
||||
Message: fmt.Sprintf("System shim directory is in PATH, but npm resolves to %s", resolved),
|
||||
}
|
||||
}
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "System shim directory is in PATH",
|
||||
ImpliesInterception: true,
|
||||
}
|
||||
}
|
||||
if userDirErr == nil && pathContainsDir(pathEntries, userDir) {
|
||||
if lookErr == nil {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusWarn,
|
||||
Message: fmt.Sprintf("Shim directory is in PATH, but npm resolves to %s", resolved),
|
||||
}
|
||||
}
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusPass,
|
||||
Message: "Shim directory is in PATH",
|
||||
ImpliesInterception: true,
|
||||
}
|
||||
}
|
||||
if shim.SystemShimsInstalled() {
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "System shim directory not in PATH",
|
||||
}
|
||||
}
|
||||
return doctor.CheckResult{
|
||||
Status: doctor.StatusFail,
|
||||
Message: "Shim directory not in PATH",
|
||||
}
|
||||
}
|
||||
|
||||
func runProtectionChecks(coreResults []doctor.CheckResult) []doctor.CheckResult {
|
||||
if !isInterceptionActive(coreResults) {
|
||||
var results []doctor.CheckResult
|
||||
@@ -344,10 +396,7 @@ func runProtectionChecks(coreResults []doctor.CheckResult) []doctor.CheckResult
|
||||
|
||||
func isInterceptionActive(coreResults []doctor.CheckResult) bool {
|
||||
for _, r := range coreResults {
|
||||
if r.Name == checkShimInPath && r.Status == doctor.StatusPass {
|
||||
return true
|
||||
}
|
||||
if r.Name == checkShellAliases && r.Status == doctor.StatusPass && r.Message == aliasesInstalledMessage {
|
||||
if r.ImpliesInterception {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ func TestPathContainsDir(t *testing.T) {
|
||||
assert.False(t, pathContainsDir([]string{"/usr/bin"}, ""))
|
||||
}
|
||||
|
||||
func TestPathIsUnderDir(t *testing.T) {
|
||||
assert.True(t, pathIsUnderDir("/usr/local/lib/pmg/bin/npm", "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathIsUnderDir("/usr/local/bin/npm", "/usr/local/lib/pmg/bin"))
|
||||
assert.False(t, pathIsUnderDir("/usr/local/lib/pmg/bin-extra/npm", "/usr/local/lib/pmg/bin"))
|
||||
}
|
||||
|
||||
func TestSystemInstallAliasesPassDoesNotActivateInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{Name: checkShellAliases, Status: doctor.StatusPass, Message: "No aliases (system install)"},
|
||||
@@ -24,9 +30,27 @@ func TestSystemInstallAliasesPassDoesNotActivateInterception(t *testing.T) {
|
||||
|
||||
func TestAliasesInstalledActivatesInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{Name: checkShellAliases, Status: doctor.StatusPass, Message: aliasesInstalledMessage},
|
||||
{
|
||||
Name: checkShellAliases,
|
||||
Status: doctor.StatusPass,
|
||||
Message: aliasesInstalledMessage,
|
||||
ImpliesInterception: true,
|
||||
},
|
||||
{Name: checkShimInPath, Status: doctor.StatusFail},
|
||||
}
|
||||
|
||||
assert.True(t, isInterceptionActive(results))
|
||||
}
|
||||
|
||||
func TestShimInPathImpliesInterception(t *testing.T) {
|
||||
results := []doctor.CheckResult{
|
||||
{
|
||||
Name: checkShimInPath,
|
||||
Status: doctor.StatusPass,
|
||||
Message: "npm resolves to system shim",
|
||||
ImpliesInterception: true,
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, isInterceptionActive(results))
|
||||
}
|
||||
|
||||
+4
-4
@@ -105,7 +105,7 @@ func install(system bool) error {
|
||||
}
|
||||
|
||||
func installSystem() error {
|
||||
if err := errIfSystemInstallAllowed(); err != nil {
|
||||
if err := requireSystemInstallSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -190,11 +190,11 @@ func remove(system, removeConfig bool) error {
|
||||
}
|
||||
|
||||
func removeSystem(removeConfig bool) error {
|
||||
if err := errIfSystemInstallAllowed(); err != nil {
|
||||
if err := requireSystemInstallSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shimMgr, err := shim.NewSystemShimManager()
|
||||
shimMgr, err := shim.NewSystemShimManagerForRemove()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create system shim manager: %w", err)
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func removeSystem(removeConfig bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func errIfSystemInstallAllowed() error {
|
||||
func requireSystemInstallSupported() error {
|
||||
if runtime.GOOS != "linux" {
|
||||
return usefulerror.NewUsefulError().
|
||||
WithCode(errcodes.UnsupportedPlatform).
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestErrIfSystemInstallAllowed(t *testing.T) {
|
||||
t.Cleanup(func() { setupGeteuid = orig })
|
||||
|
||||
setupGeteuid = func() int { return 0 }
|
||||
err := errIfSystemInstallAllowed()
|
||||
err := requireSystemInstallSupported()
|
||||
if runtime.GOOS == "linux" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
@@ -26,7 +26,7 @@ func TestErrIfSystemInstallAllowed(t *testing.T) {
|
||||
}
|
||||
|
||||
setupGeteuid = func() int { return 1000 }
|
||||
err = errIfSystemInstallAllowed()
|
||||
err = requireSystemInstallSupported()
|
||||
require.Error(t, err)
|
||||
usefulErr, ok := usefulerror.AsUsefulError(err)
|
||||
require.True(t, ok)
|
||||
|
||||
Reference in New Issue
Block a user