From f30fca636c1af9ef401e9a82ff60391cb3db97e1 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Sat, 27 Jun 2026 09:08:57 +0200 Subject: [PATCH] fix(share): delete exact directory share on trailing-slash delete (GHSA-pp88-jhwj-5qh5) DeleteWithPathPrefix queried the share index with the raw path, so deleting a directory through a trailing-slash path (e.g. DELETE /api/resources/a/) only matched descendants like /a/child and missed the exact /a share, leaving it in storage. If the same path was later recreated, the stale public share re-exposed the new content. Normalize the path before the prefix query so the exact share and its descendants are both removed. Adds a regression test. --- storage/bolt/share.go | 7 ++++--- storage/bolt/share_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/storage/bolt/share.go b/storage/bolt/share.go index 621ea6f1..49b3f0c4 100644 --- a/storage/bolt/share.go +++ b/storage/bolt/share.go @@ -78,16 +78,17 @@ func (s shareBackend) Delete(hash string) error { } func (s shareBackend) DeleteWithPathPrefix(pathPrefix string, userID uint) error { + // Share paths are stored without a trailing slash + prefix := strings.TrimRight(pathPrefix, "/") + var links []share.Link - if err := s.db.Prefix("Path", pathPrefix, &links); err != nil { + if err := s.db.Prefix("Path", prefix, &links); err != nil { if errors.Is(err, storm.ErrNotFound) { return nil } return err } - prefix := strings.TrimRight(pathPrefix, "/") - var err error for _, link := range links { if link.UserID != userID { diff --git a/storage/bolt/share_test.go b/storage/bolt/share_test.go index e303cec6..9f96a847 100644 --- a/storage/bolt/share_test.go +++ b/storage/bolt/share_test.go @@ -84,6 +84,44 @@ func TestDeleteWithPathPrefix(t *testing.T) { } } +// Regression for the trailing-slash delete leaving a stale share +// (GHSA-pp88-jhwj-5qh5): deleting "/a/" must remove the exact "/a" share and its +// descendants, not just the descendants. Siblings and other users are untouched. +func TestDeleteWithPathPrefixTrailingSlash(t *testing.T) { + t.Parallel() + + s := newTestShareBackend(t) + + links := []*share.Link{ + {Hash: "u1-a", Path: "/a", UserID: 1}, + {Hash: "u1-a-child", Path: "/a/child.txt", UserID: 1}, + {Hash: "u1-abc", Path: "/abc", UserID: 1}, // sibling sharing a byte prefix + {Hash: "u2-a", Path: "/a", UserID: 2}, // other user, must remain + } + for _, l := range links { + if err := s.Save(l); err != nil { + t.Fatalf("failed to save link %s: %v", l.Hash, err) + } + } + + // Delete with a trailing slash, as the resource delete handler does for a + // directory request like DELETE /api/resources/a/. + if err := s.DeleteWithPathPrefix("/a/", 1); err != nil { + t.Fatalf("DeleteWithPathPrefix returned error: %v", err) + } + + got := remainingHashes(t, s) + want := []string{"u1-abc", "u2-a"} + if len(got) != len(want) { + t.Fatalf("remaining hashes = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("remaining hashes = %v, want %v", got, want) + } + } +} + func TestDeleteWithPathPrefixNoMatch(t *testing.T) { t.Parallel()