File Browser has incorrect access control for public directory shares via rule path rebasing
Description
File Browser's public directory shares bypass owner deny rules because the authorization check uses a rebased relative path instead of the original owner-scoped path, letting attackers access blocked files.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
File Browser's public directory shares bypass owner deny rules because the authorization check uses a rebased relative path instead of the original owner-scoped path, letting attackers access blocked files.
Vulnerability
In File Browser versions prior to v2.63.6, the public share handlers (GET /api/public/share/* and GET /api/public/dl/*) rebase the share owner's filesystem root to the shared directory using afero.NewBasePathFs. However, the subsequent authorization check via d.Check still evaluates the request path relative to the shared directory, while the owner's deny rules are expressed relative to the original scope. This mismatch causes a path like /private/secret.txt to be checked against rules such as /projects/private, which do not match the rebased path, even though the actual filesystem path resolves to /projects/private/secret.txt. The vulnerability affects all versions before the fix committed in commit e07c59d and released in v2.63.6 [1][2][3][4].
Exploitation
An attacker who knows the URL for a public directory share can craft HTTP requests to access files and subdirectories that the share owner explicitly blocked with deny rules, as long as those blocked paths lie within the shared directory. No authentication is required because the public share endpoint is specifically designed for unauthenticated access. The attacker simply issues a GET request to h/private/secret.txt (or similar) via the public share handler, which bypasses the owner's deny rules due to the path rebasing flaw [1][2].
Impact
Successful exploitation results in unauthenticated information disclosure. The attacker can read arbitrary files or list directory contents that the owner intended to block via deny rules, provided those resources reside under the shared directory. The privilege level is unauthenticated access; the compromised scope is the shared directory and its descendants, potentially exposing sensitive data [1][2].
Mitigation
The vulnerability is fixed in File Browser version v2.63.6, released on 2026-06-12. Users should upgrade to this version immediately. The fix ensures that the authorization check is performed against the original owner-scoped path rather than the rebased relative path, as demonstrated in commit e07c59d [3][4]. There is no known workaround for versions prior to the fix; upgrading is the only mitigation.
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: <= 1.11.0
Patches
1e07c59df0b85fix: incorrect access control in public directory shares via rule path rebasing
3 files changed · +126 −0
http/data.go+16 −0 modified@@ -3,6 +3,7 @@ package fbhttp import ( "log" "net/http" + gopath "path" "strconv" "github.com/tomasen/realip" @@ -23,10 +24,25 @@ type data struct { store *storage.Storage user *users.User raw interface{} + + // checkerPrefix is prepended to every path before evaluating rules. It is + // set when the user's filesystem has been rebased onto a subdirectory (as + // done for public shares), so that rules — which are relative to the user's + // original scope — are still matched against the real path instead of the + // rebased one. Empty for regular requests. + checkerPrefix string } // Check implements rules.Checker. func (d *data) Check(path string) bool { + // When the filesystem has been rebased (e.g. a public share rooted at a + // subdirectory), the incoming path is relative to that root. Resolve it + // back to the user's original scope before matching rules, otherwise rules + // targeting paths below the share root would be silently bypassed. + if d.checkerPrefix != "" { + path = gopath.Join(d.checkerPrefix, path) + } + if d.user.HideDotfiles && rules.MatchHidden(path) { return false }
http/public.go+5 −0 modified@@ -67,6 +67,11 @@ var withHashFile = func(fn handleFunc) handleFunc { // set fs root to the shared file/folder d.user.Fs = afero.NewBasePathFs(d.user.Fs, basePath) + // the filesystem is now rebased onto basePath, so paths handed to the + // rule checker are relative to it. Resolve them back to the user's + // original scope so deny rules below the share root keep applying. + d.checkerPrefix = basePath + file, err = files.NewFileInfo(&files.FileOptions{ Fs: d.user.Fs, Path: filePath,
http/public_test.go+105 −0 modified@@ -10,6 +10,7 @@ import ( "github.com/asdine/storm/v3" "github.com/spf13/afero" + "github.com/filebrowser/filebrowser/v2/rules" "github.com/filebrowser/filebrowser/v2/settings" "github.com/filebrowser/filebrowser/v2/share" "github.com/filebrowser/filebrowser/v2/storage/bolt" @@ -143,6 +144,110 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { } } +// TestPublicShareHandlerRules ensures that owner rules keep applying to paths +// below a shared directory, even though the share rebases the filesystem onto +// that directory. A deny rule relative to the owner's scope must not be +// bypassable by requesting the blocked path through the public share. +func TestPublicShareHandlerRules(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + handler handleFunc + path string + expectedStatusCode int + }{ + "blocked file via dl handler, 403": { + handler: publicDlHandler, + path: "h/private/secret.txt", + expectedStatusCode: 403, + }, + "blocked dir listing via share handler, 403": { + handler: publicShareHandler, + path: "h/private/", + expectedStatusCode: 403, + }, + "blocked dir download via dl handler, 403": { + handler: publicDlHandler, + path: "h/private/", + expectedStatusCode: 403, + }, + "allowed file via dl handler, 200": { + handler: publicDlHandler, + path: "h/public/readme.txt", + expectedStatusCode: 200, + }, + "allowed dir listing via share handler, 200": { + handler: publicShareHandler, + path: "h/public/", + expectedStatusCode: 200, + }, + } + + for name, tc := range testCases { + name, tc := name, tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + dbPath := filepath.Join(t.TempDir(), "db") + db, err := storm.Open(dbPath) + if err != nil { + t.Fatalf("failed to open db: %v", err) + } + t.Cleanup(func() { + if err := db.Close(); err != nil { + t.Errorf("failed to close db: %v", err) + } + }) + + storage, err := bolt.NewStorage(db) + if err != nil { + t.Fatalf("failed to get storage: %v", err) + } + if err := storage.Share.Save(&share.Link{Hash: "h", UserID: 1, Path: "/projects"}); err != nil { + t.Fatalf("failed to save share: %v", err) + } + if err := storage.Users.Save(&users.User{ + Username: "username", + Password: "pw", + Perm: users.Permissions{Share: true, Download: true}, + Rules: []rules.Rule{ + {Allow: false, Path: "/projects/private"}, + }, + }); err != nil { + t.Fatalf("failed to save user: %v", err) + } + if err := storage.Settings.Save(&settings.Settings{Key: []byte("key")}); err != nil { + t.Fatalf("failed to save settings: %v", err) + } + + fs := afero.NewMemMapFs() + if err := afero.WriteFile(fs, "/projects/private/secret.txt", []byte("top secret"), 0o600); err != nil { + t.Fatalf("failed to write secret file: %v", err) + } + if err := afero.WriteFile(fs, "/projects/public/readme.txt", []byte("hello"), 0o600); err != nil { + t.Fatalf("failed to write public file: %v", err) + } + + storage.Users = &customFSUser{ + Store: storage.Users, + fs: fs, + } + + req := newHTTPRequest(t, func(r *http.Request) { r.URL.Path = tc.path }) + + recorder := httptest.NewRecorder() + handler := handle(tc.handler, "", storage, &settings.Server{}) + + handler.ServeHTTP(recorder, req) + result := recorder.Result() + defer result.Body.Close() + if result.StatusCode != tc.expectedStatusCode { + t.Errorf("expected status code %d, got status code %d", tc.expectedStatusCode, result.StatusCode) + } + }) + } +} + func newHTTPRequest(t *testing.T, requestModifiers ...func(*http.Request)) *http.Request { t.Helper() r, err := http.NewRequest(http.MethodGet, "h", http.NoBody)
Vulnerability mechanics
Root cause
"After rebasing the filesystem root onto the shared directory, the authorization checker evaluates the rebased relative path against rules that are written relative to the owner's original scope, causing a prefix-matching bypass."
Attack vector
An attacker who possesses a public directory share URL (and no authentication is required if the share lacks a password) can access files and subdirectories that the share owner explicitly blocked with rules ([ref_id=1], [ref_id=2]). By requesting a path such as `/private/secret.txt` through the public download or share handler, the rebased path bypasses the owner's deny rule for `/projects/private` because the prefix-matching logic compares the rebased relative path against the owner-scoped rule string ([ref_id=1]). This is an unauthenticated information disclosure via `GET /api/public/share/*` and `GET /api/public/dl/*`.
Affected code
The flaw exists in `http/public.go` where, after a directory share is resolved, `d.user.Fs` is replaced with `afero.NewBasePathFs(d.user.Fs, basePath)` to rebase the filesystem root onto the shared directory ([ref_id=1]). The authorization check in `http/data.go`'s `Check` method then evaluates the rebased relative path (e.g. `/private/secret.txt`) against rules that are written relative to the owner's original scope (e.g. `/projects/private`), causing a prefix-mismatch bypass ([ref_id=2]). The vulnerable endpoints are `/api/public/share/*` and `/api/public/dl/*` registered in `http/http.go`.
What the fix does
The patch introduces a `checkerPrefix` field on the `data` struct and sets it to `basePath` in `http/public.go` when the filesystem is rebased ([patch_id=5751400]). In `http/data.go`, the `Check` method now prepends `checkerPrefix` to the incoming path via `gopath.Join` before evaluating rules, so that a request for `/private/secret.txt` is resolved back to `/projects/private/secret.txt` and correctly matches the owner's deny rule ([patch_id=5751400]). The accompanying test in `http/public_test.go` verifies that blocked files and directories now return HTTP 403 instead of 200.
Preconditions
- configThe share owner must have created a public directory share (e.g., for '/projects/') that is accessible via a hash URL.
- configThe owner must have configured a deny rule (global or per-user) that blocks a path underneath that shared directory (e.g., '/projects/private').
- authThe attacker must know the share hash (e.g., exposed in a URL or guessed). No authentication is needed if the share is not password-protected.
- networkThe request is made over HTTP to the public endpoints /api/public/share/* or /api/public/dl/*.
- inputThe attacker provides a path relative to the share root that corresponds to a blocked real path (e.g., 'private/secret.txt').
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.