From 7a6b1c1ada3ba6ca2ca4ed70cf51f2f5f30d7b1e Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 18:01:49 +0200 Subject: fix(notify): cover expiry cleanup and harden the image-removal guard Split the dismiss test so expiry is exercised on its own: Expire removes the image without deleting the inert entry, and nothing asserted it. removeImage now requires filepath.Dir(filepath.Clean(path)) to equal the image directory, so a path carrying .. cannot reach a sibling daemon file such as queue.json. The previous prefix test accepted it. Not exploitable until Task 4 populates Popup.Image from a client hint, which is exactly why the guard is fixed now. --- internal/notify/service_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'internal/notify/service_test.go') diff --git a/internal/notify/service_test.go b/internal/notify/service_test.go index fb47b98..871a0c7 100644 --- a/internal/notify/service_test.go +++ b/internal/notify/service_test.go @@ -144,6 +144,40 @@ func TestCloseNotificationEmitsReasonThree(t *testing.T) { } } +func TestRemoveImageRefusesPathsOutsideTheImageDir(t *testing.T) { + dir := t.TempDir() + imgDir := ImagesDir(dir) + if err := os.MkdirAll(imgDir, 0o700); err != nil { + t.Fatal(err) + } + sibling := filepath.Join(dir, "queue.json") + if err := os.WriteFile(sibling, []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + owned := filepath.Join(imgDir, "1.png") + if err := os.WriteFile(owned, []byte("x"), 0o600); err != nil { + t.Fatal(err) + } + s := &Service{dir: dir} + + s.removeImage(filepath.Join(imgDir, "..", "queue.json")) + if _, err := os.Stat(sibling); err != nil { + t.Fatalf("traversal removed a sibling: %v", err) + } + s.removeImage(sibling) + if _, err := os.Stat(sibling); err != nil { + t.Fatalf("unrelated path removed a sibling: %v", err) + } + s.removeImage("") + if _, err := os.Stat(sibling); err != nil { + t.Fatalf("empty path removed a sibling: %v", err) + } + s.removeImage(owned) + if _, err := os.Stat(owned); !os.IsNotExist(err) { + t.Fatalf("owned image not removed: %v", err) + } +} + // A natural expiry must drop the timer entry, or the map grows by one timer per // id for the whole session. No bus is needed: the store's emit is a no-op here, // so the test exercises arm's closure directly. -- cgit v1.2.3