diff --git a/.core/TODO.md b/.core/TODO.md index e69de29b..874ce2e8 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -0,0 +1 @@ +- @security pkg/display/display.go:954 — WebSocket layout commands still coerce missing or malformed numeric fields to zero instead of rejecting the request. diff --git a/pkg/environment/service.go b/pkg/environment/service.go index 8aed0b03..18d9f93b 100644 --- a/pkg/environment/service.go +++ b/pkg/environment/service.go @@ -3,6 +3,7 @@ package environment import ( "context" + "path/filepath" "strings" "sync" @@ -42,7 +43,11 @@ func (s *Service) OnStartup(_ context.Context) core.Result { }) s.Core().Action("environment.openFileManager", func(_ context.Context, opts core.Options) core.Result { t, _ := opts.Get("task").Value.(TaskOpenFileManager) - if err := s.platform.OpenFileManager(t.Path, t.Select); err != nil { + path, err := validatedOpenFileManagerPath(t.Path) + if err != nil { + return core.Result{Value: err, OK: false} + } + if err := s.platform.OpenFileManager(path, t.Select); err != nil { return core.Result{Value: err, OK: false} } return core.Result{OK: true} @@ -160,6 +165,21 @@ func normalizeTheme(theme string) (string, error) { } } +func validatedOpenFileManagerPath(raw string) (string, error) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "", coreerr.E("environment.openFileManager", "path is required", nil) + } + if strings.ContainsRune(trimmed, '\x00') { + return "", coreerr.E("environment.openFileManager", "path contains a null byte", nil) + } + cleaned := filepath.Clean(trimmed) + if !filepath.IsAbs(cleaned) { + return "", coreerr.E("environment.openFileManager", "path must be absolute", nil) + } + return cleaned, nil +} + func themeName(isDark bool) string { if isDark { return "dark" diff --git a/pkg/environment/service_test.go b/pkg/environment/service_test.go index ff2bb680..12883f75 100644 --- a/pkg/environment/service_test.go +++ b/pkg/environment/service_test.go @@ -3,6 +3,7 @@ package environment import ( "context" + "path/filepath" "sync" "testing" @@ -17,6 +18,8 @@ type mockPlatform struct { info EnvironmentInfo accentColour string openFMErr error + openFMPath string + openFMSelect bool focusFollowsMouse bool themeHandler func(isDark bool) mu sync.Mutex @@ -27,6 +30,8 @@ func (m *mockPlatform) Info() EnvironmentInfo { return m.info } func (m *mockPlatform) AccentColour() string { return m.accentColour } func (m *mockPlatform) HasFocusFollowsMouse() bool { return m.focusFollowsMouse } func (m *mockPlatform) OpenFileManager(path string, selectFile bool) error { + m.openFMPath = path + m.openFMSelect = selectFile return m.openFMErr } func (m *mockPlatform) OnThemeChange(handler func(isDark bool)) func() { @@ -100,11 +105,22 @@ func TestQueryAccentColour_Good(t *testing.T) { } func TestTaskOpenFileManager_Good(t *testing.T) { - _, c := newTestService(t) + mock, c := newTestService(t) r := c.Action("environment.openFileManager").Run(context.Background(), core.NewOptions( core.Option{Key: "task", Value: TaskOpenFileManager{Path: "/tmp", Select: true}}, )) require.True(t, r.OK) + assert.Equal(t, filepath.Clean("/tmp"), mock.openFMPath) + assert.True(t, mock.openFMSelect) +} + +func TestTaskOpenFileManager_Bad_InvalidPath(t *testing.T) { + _, c := newTestService(t) + r := c.Action("environment.openFileManager").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: TaskOpenFileManager{Path: "../tmp", Select: false}}, + )) + assert.False(t, r.OK) + assert.Contains(t, r.Value.(error).Error(), "path must be absolute") } func TestThemeChange_ActionBroadcast_Good(t *testing.T) {