Harden update manager directory scans

This commit is contained in:
rcourtman 2026-03-29 14:23:13 +01:00
parent 82c8c08a3e
commit 4af6858405
3 changed files with 56 additions and 5 deletions

View file

@ -151,6 +151,11 @@ update server bases must normalize to absolute HTTP(S) URLs without userinfo,
and release API, feed, download, and checksum requests must resolve from
validated URL objects instead of raw string concatenation or request creation
from unchecked inputs.
That same boundary also governs owned filesystem scans inside the update
manager: when `internal/updates/manager.go` enumerates already-owned extract,
temp, backup, or restore directories, it must rejoin discovered entry names
through the shared storage-path helper instead of rebuilding raw
`filepath.Join(dir, entry.Name())` paths.
That same boundary also governs install.sh rollback restore targets:
`adapter_installsh.go` may not hardcode `/etc/pulse` for rollback safety
backups or config restore, and must derive the rollback config directory

View file

@ -1681,8 +1681,16 @@ func (m *Manager) applyUpdateFiles(extractDir string) error {
}
// Copy agent binaries
if strings.HasPrefix(name, "pulse-agent-") {
srcPath := filepath.Join(binDir, name)
destPath := filepath.Join(destBinDir, name)
srcPath, err := securityutil.JoinStorageLeaf(binDir, name)
if err != nil {
log.Warn().Err(err).Str("binary", name).Msg("Skipping invalid agent binary path")
continue
}
destPath, err := securityutil.JoinStorageLeaf(destBinDir, name)
if err != nil {
log.Warn().Err(err).Str("binary", name).Msg("Skipping invalid destination binary path")
continue
}
cmd = exec.Command("cp", "-a", srcPath, destPath)
if err := cmd.Run(); err != nil {
log.Warn().Err(err).Str("binary", name).Msg("Failed to copy agent binary")
@ -1855,7 +1863,11 @@ func (m *Manager) cleanupOldTempDirs() {
continue
}
fullPath := filepath.Join(dir, entry.Name())
fullPath, err := securityutil.JoinStorageLeaf(dir, entry.Name())
if err != nil {
log.Debug().Err(err).Str("dir", dir).Str("entry", entry.Name()).Msg("Skipping invalid temp directory leaf")
continue
}
info, err := os.Stat(fullPath)
if err != nil {
continue
@ -1937,8 +1949,16 @@ func (m *Manager) copyDirSafe(src, dest string) error {
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
destPath := filepath.Join(dest, entry.Name())
srcPath, err := securityutil.JoinStorageLeaf(src, entry.Name())
if err != nil {
log.Warn().Err(err).Str("dir", src).Str("entry", entry.Name()).Msg("Skipping invalid source path during backup/restore")
continue
}
destPath, err := securityutil.JoinStorageLeaf(dest, entry.Name())
if err != nil {
log.Warn().Err(err).Str("dir", dest).Str("entry", entry.Name()).Msg("Skipping invalid destination path during backup/restore")
continue
}
// Get file info (using Lstat to detect symlinks)
info, err := os.Lstat(srcPath)

View file

@ -137,3 +137,29 @@ func TestManagerCopyDirSafe(t *testing.T) {
t.Fatal("expected symlink to be skipped")
}
}
func TestManagerCopyDirSafe_SkipsInvalidLeafNames(t *testing.T) {
manager := &Manager{}
srcDir := filepath.Join(t.TempDir(), "src")
destDir := filepath.Join(t.TempDir(), "dest")
if err := os.MkdirAll(srcDir, 0755); err != nil {
t.Fatalf("mkdir src: %v", err)
}
if err := os.WriteFile(filepath.Join(srcDir, "ok.txt"), []byte("ok"), 0600); err != nil {
t.Fatalf("write ok: %v", err)
}
if err := os.WriteFile(filepath.Join(srcDir, `bad\name.txt`), []byte("skip"), 0600); err != nil {
t.Fatalf("write bad leaf: %v", err)
}
if err := manager.copyDirSafe(srcDir, destDir); err != nil {
t.Fatalf("copyDirSafe error: %v", err)
}
if _, err := os.ReadFile(filepath.Join(destDir, "ok.txt")); err != nil {
t.Fatalf("expected ok.txt copied: %v", err)
}
if _, err := os.Lstat(filepath.Join(destDir, `bad\name.txt`)); err == nil {
t.Fatal("expected invalid leaf to be skipped")
}
}