File Browser: Cross-user unauthorized share-link deletion via unbounded prefix match in DeleteWithPathPrefix
Description
A low-privileged filebrowser user with create/delete permissions can silently destroy any other user's share links by deleting a file whose logical path is a byte-prefix of the victim's share path.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A low-privileged filebrowser user with create/delete permissions can silently destroy any other user's share links by deleting a file whose logical path is a byte-prefix of the victim's share path.
Vulnerability
The vulnerability resides in the resourceDeleteHandler function in http/resource.go, which calls d.store.Share.DeleteWithPathPrefix(file.Path) to clean up share records when a file is deleted. The file.Path argument is the logical path from the deleting user's request URL (e.g., /a), not the absolute filesystem path. This path is passed directly to the bolt backend in storage/bolt/share.go, which uses s.db.Prefix("Path", pathPrefix, &links) to match any share links whose Path field begins with that prefix. Because DeleteWithPathPrefix does not check the UserID field of matched links, a low-privileged user with create and delete permissions in their own scope can delete a file whose logical path is a byte-prefix of another user's stored share.Link.Path, thereby wiping those foreign share links. The bug affects filebrowser versions up to and including 2.63.5 [1][3].
Exploitation
An attacker must have an authenticated filebrowser account with create and delete permissions in their own isolated directory scope. The attacker chooses a short file path (e.g., /a, /b, /c) that is a byte-prefix of many possible share link paths. The attacker then sends a legitimate DELETE request for that file within their own scope. The server passes the logical path (e.g., /a) to DeleteWithPathPrefix, which fetches all share links with a Path starting with /a — including links belonging to other users (e.g., /admin/doc, /a/secret) — and deletes them from the database. The attacker can repeat this with different short prefixes to systematically wipe a large portion of share links, even without knowing the exact paths of victims' shares [2][3].
Impact
Successful exploitation results in unauthorized deletion of share-link metadata belonging to arbitrary users, including administrators. This is an integrity breach (tampering with share records) and also an availability impact, as the victim's share links are irrevocably removed, effectively denying the share-link feature for affected users. An attacker with low privileges can therefore deny service to the entire share-link system by iterating a short set of one- and two-character prefixes [1][3].
Mitigation
The fix was released in filebrowser version 2.63.6 on 2026-06-12. The commit 0231b7e modifies DeleteWithPathPrefix to accept a user ID parameter and filter matched links by UserID, preventing cross-user deletion [2][4]. Users are strongly advised to upgrade to 2.63.6 or later. No workaround is available for older versions, as the vulnerable function is called automatically on file deletion.
AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <= 2.63.5
Patches
10231b7ebdfbefix: cross-user unauthorized share-link deletion
5 files changed · +117 −6
http/resource.go+1 −1 modified@@ -100,7 +100,7 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc { return errToStatus(err), err } - err = d.store.Share.DeleteWithPathPrefix(file.Path) + err = d.store.Share.DeleteWithPathPrefix(file.Path, d.user.ID) if err != nil { log.Printf("WARNING: Error(s) occurred while deleting associated shares with file: %s", err) }
share/storage.go+3 −3 modified@@ -15,7 +15,7 @@ type StorageBackend interface { Gets(path string, id uint) ([]*Link, error) Save(s *Link) error Delete(hash string) error - DeleteWithPathPrefix(path string) error + DeleteWithPathPrefix(path string, userID uint) error } // Storage is a storage. @@ -137,6 +137,6 @@ func (s *Storage) Delete(hash string) error { return s.back.Delete(hash) } -func (s *Storage) DeleteWithPathPrefix(path string) error { - return s.back.DeleteWithPathPrefix(path) +func (s *Storage) DeleteWithPathPrefix(path string, userID uint) error { + return s.back.DeleteWithPathPrefix(path, userID) }
share/storage_test.go+1 −1 modified@@ -59,7 +59,7 @@ func (f fakeBackend) Delete(_ string) error { return nil } -func (f fakeBackend) DeleteWithPathPrefix(_ string) error { +func (f fakeBackend) DeleteWithPathPrefix(_ string, _ uint) error { return nil }
storage/bolt/share.go+15 −1 modified@@ -2,6 +2,7 @@ package bolt import ( "errors" + "strings" "github.com/asdine/storm/v3" "github.com/asdine/storm/v3/q" @@ -76,14 +77,27 @@ func (s shareBackend) Delete(hash string) error { return err } -func (s shareBackend) DeleteWithPathPrefix(pathPrefix string) error { +func (s shareBackend) DeleteWithPathPrefix(pathPrefix string, userID uint) error { var links []share.Link if err := s.db.Prefix("Path", pathPrefix, &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 { + continue + } + + if link.Path != prefix && !strings.HasPrefix(link.Path, prefix+"/") { + continue + } + err = errors.Join(err, s.db.DeleteStruct(&share.Link{Hash: link.Hash})) } return err
storage/bolt/share_test.go+97 −0 added@@ -0,0 +1,97 @@ +package bolt + +import ( + "os" + "sort" + "testing" + + "github.com/asdine/storm/v3" + + "github.com/filebrowser/filebrowser/v2/share" +) + +func newTestShareBackend(t *testing.T) shareBackend { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "shares-*.db") + if err != nil { + t.Fatalf("failed to create temp db: %v", err) + } + _ = f.Close() + + db, err := storm.Open(f.Name()) + if err != nil { + t.Fatalf("failed to open db: %v", err) + } + t.Cleanup(func() { _ = db.Close() }) + + return shareBackend{db: db} +} + +func remainingHashes(t *testing.T, s shareBackend) []string { + t.Helper() + + links, err := s.All() + if err != nil { + t.Fatalf("All returned error: %v", err) + } + + hashes := make([]string, 0, len(links)) + for _, link := range links { + hashes = append(hashes, link.Hash) + } + sort.Strings(hashes) + return hashes +} + +func TestDeleteWithPathPrefix(t *testing.T) { + t.Parallel() + + s := newTestShareBackend(t) + + links := []*share.Link{ + // user 1's links + {Hash: "u1-a", Path: "/a", UserID: 1}, + {Hash: "u1-a-child", Path: "/a/child.txt", UserID: 1}, + {Hash: "u1-abc", Path: "/abc", UserID: 1}, // not a descendant of /a + {Hash: "u1-other", Path: "/other", UserID: 1}, + // user 2's links — must never be touched when user 1 deletes + {Hash: "u2-a", Path: "/a", UserID: 2}, + {Hash: "u2-a-child", Path: "/a/child.txt", UserID: 2}, + } + for _, l := range links { + if err := s.Save(l); err != nil { + t.Fatalf("failed to save link %s: %v", l.Hash, err) + } + } + + // User 1 deletes their directory /a. Only user 1's /a and its descendants + // should be removed; /abc (sibling sharing a byte prefix) and all of user + // 2's links must remain. + if err := s.DeleteWithPathPrefix("/a", 1); err != nil { + t.Fatalf("DeleteWithPathPrefix returned error: %v", err) + } + + got := remainingHashes(t, s) + want := []string{"u1-abc", "u1-other", "u2-a", "u2-a-child"} + 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() + + s := newTestShareBackend(t) + + // No links exist at all: the storm Prefix query returns ErrNotFound, which + // must be treated as a no-op rather than surfaced as an error. + if err := s.DeleteWithPathPrefix("/a", 1); err != nil { + t.Fatalf("DeleteWithPathPrefix on empty store returned error: %v", err) + } +}
Vulnerability mechanics
Root cause
"Missing user-ownership check in DeleteWithPathPrefix allows cross-user share-link deletion via unbounded prefix matching"
Attack vector
A low-privileged authenticated user with `create` and `delete` permissions in their own isolated scope sends a legitimate DELETE request for a file whose logical path (e.g., `/a`) is a byte-prefix of another user's stored `share.Link.Path`. The `resourceDeleteHandler` in `http/resource.go` calls `d.store.Share.DeleteWithPathPrefix(file.Path)` without checking the `UserID` of the share records [ref_id=1]. The Bolt backend uses `db.Prefix('Path', pathPrefix, &links)` which matches all share links whose path starts with the given prefix, regardless of ownership [patch_id=5749830]. This allows an attacker to silently destroy share-link records belonging to any other user, including the administrator, without exposing file contents.
What the fix does
The patch modifies `DeleteWithPathPrefix` in `storage/bolt/share.go` to accept a `userID` parameter [patch_id=5749830]. It now filters results by `link.UserID != userID` and performs an exact path-match check (`link.Path != prefix && !strings.HasPrefix(link.Path, prefix+'/')`) to prevent byte-prefix collisions like `/a` matching `/abc` [patch_id=5749830]. The `ErrNotFound` case is also handled as a no-op. The caller in `http/resource.go` is updated to pass `d.user.ID`, ensuring that a user can only delete share links they own. The storage interface and test suite are updated accordingly.
Preconditions
- authAttacker must have an authenticated account with 'create' and 'delete' permissions within their own scoped directory
- inputThe attacker's deleted file's logical path must be a byte-prefix of another user's stored share.Link.Path
Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.