1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 20:15:07 +00:00

security: fix CodeQL command injection and path traversal alerts

- Add validatePath() helper to check path safety before external commands
- Validate paths in delete.go (moveToTrash), scanner.go (mdfind, du),
  and main.go (open command)
- Remove overly restrictive character whitelist that rejected valid
  macOS paths (Chinese, emoji, $, ;, etc.)
- Unify path validation logic across all three files

Fixes CodeQL alerts:
- Command injection in osascript (delete.go)
- Command injection in mdfind/du (scanner.go)
- Path traversal in open command (main.go)
This commit is contained in:
Tw93
2026-03-14 08:24:08 +08:00
parent f6acfa774c
commit 951e395ab7
3 changed files with 67 additions and 24 deletions

View File

@@ -126,6 +126,11 @@ func moveToTrash(path string) error {
return fmt.Errorf("failed to resolve path: %w", err) return fmt.Errorf("failed to resolve path: %w", err)
} }
// Validate path to prevent path traversal attacks.
if err := validatePath(absPath); err != nil {
return err
}
// Escape path for AppleScript (handle quotes and backslashes). // Escape path for AppleScript (handle quotes and backslashes).
escapedPath := strings.ReplaceAll(absPath, "\\", "\\\\") escapedPath := strings.ReplaceAll(absPath, "\\", "\\\\")
escapedPath = strings.ReplaceAll(escapedPath, "\"", "\\\"") escapedPath = strings.ReplaceAll(escapedPath, "\"", "\\\"")
@@ -146,3 +151,23 @@ func moveToTrash(path string) error {
return nil return nil
} }
// validatePath checks path safety for external commands.
// Returns error if path is empty, relative, contains null bytes, or escapes root.
func validatePath(path string) error {
if path == "" {
return fmt.Errorf("path is empty")
}
if !filepath.IsAbs(path) {
return fmt.Errorf("path must be absolute: %s", path)
}
if strings.Contains(path, "\x00") {
return fmt.Errorf("path contains null bytes")
}
// Ensure Clean doesn't radically alter the path (path traversal check).
clean := filepath.Clean(path)
if !strings.HasPrefix(clean, "/") {
return fmt.Errorf("path escapes root: %s", path)
}
return nil
}

View File

@@ -775,18 +775,14 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
for path := range m.largeMultiSelected { for path := range m.largeMultiSelected {
go func(p string) { go func(p string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(p, false)
defer cancel()
_ = exec.CommandContext(ctx, "open", p).Run()
}(path) }(path)
} }
m.status = fmt.Sprintf("Opening %d items...", count) m.status = fmt.Sprintf("Opening %d items...", count)
} else { } else {
selected := m.largeFiles[m.largeSelected] selected := m.largeFiles[m.largeSelected]
go func(path string) { go func(path string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(path, false)
defer cancel()
_ = exec.CommandContext(ctx, "open", path).Run()
}(selected.Path) }(selected.Path)
m.status = fmt.Sprintf("Opening %s...", selected.Name) m.status = fmt.Sprintf("Opening %s...", selected.Name)
} }
@@ -800,18 +796,14 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
for path := range m.multiSelected { for path := range m.multiSelected {
go func(p string) { go func(p string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(p, false)
defer cancel()
_ = exec.CommandContext(ctx, "open", p).Run()
}(path) }(path)
} }
m.status = fmt.Sprintf("Opening %d items...", count) m.status = fmt.Sprintf("Opening %d items...", count)
} else { } else {
selected := m.entries[m.selected] selected := m.entries[m.selected]
go func(path string) { go func(path string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(path, false)
defer cancel()
_ = exec.CommandContext(ctx, "open", path).Run()
}(selected.Path) }(selected.Path)
m.status = fmt.Sprintf("Opening %s...", selected.Name) m.status = fmt.Sprintf("Opening %s...", selected.Name)
} }
@@ -829,18 +821,14 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
for path := range m.largeMultiSelected { for path := range m.largeMultiSelected {
go func(p string) { go func(p string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(p, true)
defer cancel()
_ = exec.CommandContext(ctx, "open", "-R", p).Run()
}(path) }(path)
} }
m.status = fmt.Sprintf("Showing %d items in Finder...", count) m.status = fmt.Sprintf("Showing %d items in Finder...", count)
} else { } else {
selected := m.largeFiles[m.largeSelected] selected := m.largeFiles[m.largeSelected]
go func(path string) { go func(path string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(path, true)
defer cancel()
_ = exec.CommandContext(ctx, "open", "-R", path).Run()
}(selected.Path) }(selected.Path)
m.status = fmt.Sprintf("Showing %s in Finder...", selected.Name) m.status = fmt.Sprintf("Showing %s in Finder...", selected.Name)
} }
@@ -854,18 +842,14 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
for path := range m.multiSelected { for path := range m.multiSelected {
go func(p string) { go func(p string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(p, true)
defer cancel()
_ = exec.CommandContext(ctx, "open", "-R", p).Run()
}(path) }(path)
} }
m.status = fmt.Sprintf("Showing %d items in Finder...", count) m.status = fmt.Sprintf("Showing %d items in Finder...", count)
} else { } else {
selected := m.entries[m.selected] selected := m.entries[m.selected]
go func(path string) { go func(path string) {
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout) _ = safeOpen(path, true)
defer cancel()
_ = exec.CommandContext(ctx, "open", "-R", path).Run()
}(selected.Path) }(selected.Path)
m.status = fmt.Sprintf("Showing %s in Finder...", selected.Name) m.status = fmt.Sprintf("Showing %s in Finder...", selected.Name)
} }
@@ -1172,3 +1156,17 @@ func scanOverviewPathCmd(path string, index int) tea.Cmd {
} }
} }
} }
// safeOpen executes 'open' command with path validation.
func safeOpen(path string, reveal bool) error {
if err := validatePath(path); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout)
defer cancel()
args := []string{path}
if reveal {
args = []string{"-R", path}
}
return exec.CommandContext(ctx, "open", args...).Run()
}

View File

@@ -409,6 +409,16 @@ func calculateDirSizeFast(root string, filesScanned, dirsScanned, bytesScanned *
// Use Spotlight (mdfind) to quickly find large files. // Use Spotlight (mdfind) to quickly find large files.
func findLargeFilesWithSpotlight(root string, minSize int64) []fileEntry { func findLargeFilesWithSpotlight(root string, minSize int64) []fileEntry {
// Validate root path.
if err := validatePath(root); err != nil {
return nil
}
// Validate minSize is reasonable (non-negative and not excessively large).
if minSize < 0 || minSize > 1<<50 { // 1 PB max
return nil
}
query := fmt.Sprintf("kMDItemFSSize >= %d", minSize) query := fmt.Sprintf("kMDItemFSSize >= %d", minSize)
ctx, cancel := context.WithTimeout(context.Background(), mdlsTimeout) ctx, cancel := context.WithTimeout(context.Background(), mdlsTimeout)
@@ -635,6 +645,16 @@ func getDirectorySizeFromDu(path string) (int64, error) {
} }
func getDirectorySizeFromDuWithExclude(path string, excludePath string) (int64, error) { func getDirectorySizeFromDuWithExclude(path string, excludePath string) (int64, error) {
// Validate paths.
if err := validatePath(path); err != nil {
return 0, err
}
if excludePath != "" {
if err := validatePath(excludePath); err != nil {
return 0, err
}
}
runDuSize := func(target string) (int64, error) { runDuSize := func(target string) (int64, error) {
if _, err := os.Stat(target); err != nil { if _, err := os.Stat(target); err != nil {
return 0, err return 0, err