DOKIMASecurity review report

Overview

Shipped default scope "." resolves to server root, granting self-registered accounts whole-tree read/write/delete

high Possibly Valid medium confidence

DOK-100099-FILEBROWSER-SETTINGS-CONFIG-ENABLING-SIGNUP-WI · filebrowser · settings-config

Status

Possibly Valid

Valid insecure-default / authorization-isolation finding. Every cited line was re-verified against current source and the anonymous-registrant-to-whole-served-root read/write/delete path is complete and deterministic given the shipped defaults; the peer review independently returned "valid" and the finding is materially distinct from the prior empty-scope dismissals (shipped "." resolves to "/" with zero configuration). Downgraded from fully_valid to possibly_valid because the elevated cross-user-breach severity depends on two deployment facts source cannot settle — Signup being enabled and the presence of other narrower-scoped tenants/sensitive data under server.Root — which are named precisely in the blocker; the always-true core impact (anonymous whole-root R/W/delete once Signup is on) remains fully evidenced.

Repository / Component

Repository
filebrowser
Component
settings-config

Plain-English Description

With the built-in default settings, anyone who signs up for an account is placed at the very top of the server's file tree instead of a private folder of their own. That gives every self-registered user permission to read, change, and delete all served files.

Description of the Underlying Issue

The shipped factory defaults are CreateUserDir=false and Defaults.Scope="." (cmd/root.go:398-418). When self-signup is enabled, signupHandler applies these defaults (Scope="."), and because CreateUserDir=false the CreateUserDir scope-reset branch (auth.go:186-188) is skipped, so user.Scope stays ".". MakeUserDir (settings/dir.go:22-33) computes path.Join("/", ".") == "/" and persists Scope="/". On each request, withUser -> User.Clean roots the user's afero.BasePathFs at filepath.Join(server.Root, "/") == server.Root (users/users.go:93-96), so the account's filesystem is the entire served tree. Combined with the default permissions (Create/Rename/Modify/Delete/Share/Download all true; only Admin and Execute stripped), a self-registered account can read, create, rename, modify, delete, share, and download every file under server.Root. The same MakeUserDir sink is used by the proxy (auth/proxy.go:48,54) and hook (auth/hook.go:163,176) provisioners, which omit signup's CreateUserDir reset entirely and therefore resolve to "/" even when CreateUserDir=true. This is materially distinct from prior dismissed empty-scope concerns, which framed an admin-CHOSEN empty string as misconfiguration; here the factory default "." resolves identically to "/" with zero scope configuration.

Potential Attack

1) An admin enables the single supported toggle Settings.Signup=true while leaving the shipped CreateUserDir=false and Defaults.Scope=".". 2) An unauthenticated attacker POSTs to /api/signup with {username,password}. 3) signupHandler applies Defaults, does not reset Scope, strips Admin+Execute, calls MakeUserDir which returns "/", and persists user.Scope="/". 4) The attacker authenticates; on every subsequent request their filesystem is rooted at the entire server.Root, so all file APIs operate over the whole served tree.

Outcomes of Potential Attack

An anonymous registrant obtains read/create/rename/modify/delete/share/download over every file under server.Root. In a mixed deployment where other users are given narrower scopes or sensitive data lives under the root, this is a cross-user confidentiality and integrity breach. Execute and Admin are stripped, so this specific path is not remote code execution.

Affected Scope

Every self-registered account created via the unauthenticated POST /api/signup route, plus proxy- and hook-auto-provisioned accounts, whenever the instance runs with the shipped defaults (CreateUserDir=false, Defaults.Scope="."). Each such account is confined to a BasePathFs rooted at the entire server.Root with the default Create/Rename/Modify/Delete/Share/Download permissions.

Suggested Fix (plain english)

Never let an auto-provisioned account's scope resolve to the server root; give each self-registered user its own isolated subfolder, or change the default scope away from ".

Suggested Fix (detailed)

Refuse or floor any auto-provisioned (signup/proxy/hook) scope that resolves to "/" (server.Root) down to an isolated per-user subtree, and treat a MakeUserDir result of "/" for auto-provisioned users as an error. Alternatively change the shipped Defaults.Scope to a per-user path, require CreateUserDir=true whenever Signup is enabled, and/or warn on settings save when Signup=true combines with a root-resolving default scope. Apply the same guard to auth/proxy.go and auth/hook.go, which resolve to "/" even when CreateUserDir=true. Verify that no provisioning path persists Scope="/" and that a self-registered account's afero root is a strict subdirectory of server.Root. Refuse or floor any auto-provisioned (signup/proxy/hook) scope that resolves to "/" (server.Root) down to an isolated per-user subtree; treat a MakeUserDir result of "/" for auto-provisioned users as an error. Alternatively change the shipped Defaults.Scope to a per-user path, or require CreateUserDir=true whenever Signup is enabled, and/or warn on settings save when Signup=true is combined with a root-resolving default scope. Apply the same guard to auth/proxy.go and auth/hook.go, which resolve to "/" even when CreateUserDir=true.

Validation

Valid insecure-default / authorization-isolation finding. Every cited line was re-verified against current source and the anonymous-registrant-to-whole-served-root read/write/delete path is complete and deterministic given the shipped defaults; the peer review independently returned "valid" and the finding is materially distinct from the prior empty-scope dismissals (shipped "." resolves to "/" with zero configuration). Downgraded from fully_valid to possibly_valid because the elevated cross-user-breach severity depends on two deployment facts source cannot settle — Signup being enabled and the presence of other narrower-scoped tenants/sensitive data under server.Root — which are named precisely in the blocker; the always-true core impact (anonymous whole-root R/W/delete once Signup is on) remains fully evidenced.

first_opinion

Full Evidence

cmd/root.go:398-418 quickSetup shipped defaults: Signup=false, CreateUserDir=false, Defaults.Scope=".", Defaults.Perm Create/Rename/Modify/Delete/Share/Download=true, Admin=false
http/http.go:50 api.Handle("/signup", monkey(signupHandler, "")) — unauthenticated route (empty permission), no withUser/withAdmin wrapper
http/auth.go:145-148,160 signupHandler is gated only by d.settings.Signup and accepts an untrimmed, non-empty attacker-supplied username/password
http/auth.go:168 d.settings.Defaults.Apply(user) and settings/defaults.go:26 Apply sets u.Scope=d.Scope (=".")
http/auth.go:186-188 the `if d.settings.CreateUserDir { user.Scope = "" }` reset is SKIPPED when CreateUserDir=false, so user.Scope remains "."
settings/dir.go:22-33 MakeUserDir: userScope=TrimSpace(".")="."; the `userScope=="" && s.CreateUserDir` branch is not entered because "."!=""; userScope=path.Join("/",".")="/"; MkdirAll("/") on BasePathFs(serverRoot) is idempotent and returns "/"
http/auth.go:195 user.Scope=userHome (="/") persisted via d.store.Users.Save
http/auth.go:103 withUser loads the user with baseScope=d.server.Root; users/users.go:93-96 User.Clean sets scope=filepath.Join(server.Root, filepath.Join("/","/"))=server.Root and u.Fs=afero.NewBasePathFs(OsFs, server.Root) — confinement equals the entire served root
auth/proxy.go:48,54 and auth/hook.go:163,176 apply the same Defaults.Scope="." through MakeUserDir and, unlike signup, never reset Scope for CreateUserDir, so those provisioners resolve to "/" regardless of CreateUserDir
Proven fact: The vulnerable code path is complete and deterministic in current source: with the shipped defaults, an unauthenticated POST /api/signup persists an account whose Scope resolves to "/" and whose afero.BasePathFs is rooted at the entire server.Root.
Proven fact: path.Join("/",".")=="/" and filepath.Join(serverRoot, filepath.Join("/","/"))==serverRoot (Go path/filepath cleaning semantics), so the registrant's filesystem root equals the whole served tree.
Proven fact: With the default Perm (Create/Rename/Modify/Delete/Share/Download all true) and signup forcing only Admin=false/Execute=false, the registrant can read, create, rename, modify, delete, share, and download every file under server.Root. Execute/Admin are stripped so this specific path is not RCE.
Proven fact: This is materially distinct from the two previously dismissed "empty Defaults.Scope -> /" concerns: those computed path.Join("/","") for an admin-CHOSEN empty string and framed it as admin misconfiguration; the shipped factory default "." resolves identically to "/" with ZERO scope configuration, which is a reachability change the dismissals never evaluated. The independent peer review verdict was "valid".
Proven fact: The proxy (auth/proxy.go) and hook (auth/hook.go) provisioners share the same MakeUserDir sink and omit signup's CreateUserDir reset entirely, so they resolve Defaults.Scope="." to "/" even when CreateUserDir=true.
Unvalidated fact: Whether any specific deployment has enabled Settings.Signup=true — this is a non-default admin toggle (shipped default false at cmd/root.go:398) and cannot be settled by reading source; it gates the unauthenticated reachability of the whole chain.
Unvalidated fact: Whether the served server.Root actually hosts other tenants' home directories or sensitive data whose exposure constitutes a cross-user confidentiality/integrity breach. Under pure shipped defaults, admin-created users ALSO receive scope "."->"/", so there may be no narrower-scoped tenants to breach; the elevated cross-user-isolation impact requires a MIXED deployment (some users given restrictive scopes) that source cannot confirm.
Unvalidated fact: The proxy/hook variants additionally require AuthMethod=proxy or =hook (admin-configured) and an upstream/hook trust relationship; only the shared code path — not the surrounding proxy/hook trust model — was validated here.
cmd/root.go:398-418 — quickSetup shipped defaults: Signup=false, CreateUserDir=false, Defaults.Scope=".", default Perm Create/Rename/Modify/Delete/Share/Download=true, Admin=false
http/http.go:50 — api.Handle("/signup", monkey(signupHandler, "")): unauthenticated route, no withUser/withAdmin wrapper
http/auth.go:145-148,160 — signupHandler gated only by d.settings.Signup, accepts untrimmed non-empty attacker username/password
http/auth.go:168 + settings/defaults.go:26 — Defaults.Apply sets u.Scope=d.Scope (=".")
http/auth.go:186-188 — the `if d.settings.CreateUserDir { user.Scope = "" }` reset is skipped when CreateUserDir=false
settings/dir.go:22-33 — MakeUserDir: userScope=TrimSpace(".")=".", path.Join("/",".")="/", MkdirAll("/") idempotent, returns "/"
http/auth.go:195 — user.Scope=userHome (="/") persisted
http/auth.go:103 + users/users.go:93-96 — withUser/User.Clean roots u.Fs at afero.NewBasePathFs(OsFs, server.Root); scope=filepath.Join(server.Root, "/")=server.Root
auth/proxy.go:48,54 and auth/hook.go:163,176 — same MakeUserDir sink, no CreateUserDir reset, resolve "."->"/" even when CreateUserDir=true
Proven fact: With shipped defaults, an unauthenticated POST /api/signup persists an account whose Scope resolves to "/" and whose afero.BasePathFs is rooted at the entire server.Root.
Proven fact: path.Join("/",".")=="/" and filepath.Join(serverRoot, filepath.Join("/","/"))==serverRoot per Go path/filepath semantics, so the registrant's filesystem root equals the whole served tree.
Proven fact: With default Perm and signup stripping only Admin/Execute, the registrant can read/create/rename/modify/delete/share/download every file under server.Root; Execute/Admin stripped so this path is not RCE.
Proven fact: This is distinct from the prior dismissed empty-scope concerns: those were admin-chosen empty strings framed as misconfiguration; the factory default "." resolves to "/" with zero configuration, a reachability change the dismissals never evaluated. Independent peer review returned "valid".
Proven fact: The proxy and hook provisioners share the same MakeUserDir sink and omit signup's CreateUserDir reset, so they resolve "."->"/" even when CreateUserDir=true.
Unvalidated fact: Whether any specific deployment has enabled Settings.Signup=true — a non-default admin toggle (shipped default false) that gates unauthenticated reachability of the whole chain.
Unvalidated fact: Whether server.Root actually hosts other tenants' home directories or sensitive data. Under pure shipped defaults admin-created users ALSO get scope "."->"/", so a MIXED deployment (some users given restrictive scopes) is required for the elevated cross-user-isolation impact.
Unvalidated fact: The proxy/hook variants additionally require AuthMethod=proxy or =hook plus an upstream/hook trust relationship; only the shared code path was validated, not the surrounding proxy/hook trust model.
cmd/root.go:398,400,404,410-417 (shipped defaults: Signup=false, CreateUserDir=false, Defaults.Scope='.', Perm Create/Rename/Modify/Delete/Share/Download=true, Admin=false)
settings/dir.go:22-33 (CreateUserDir=false skips per-user branch; userScope=path.Join('/', '.')='/' )
http/auth.go:145-195 (signup: gate on Settings.Signup; Defaults.Apply; MakeUserDir; user.Scope=userHome='/')
http/http.go:50 (unauthenticated POST /api/signup route)
settings/defaults.go:25-26 (Apply sets u.Scope=d.Scope)
users/users.go:93-96,103-104 (scope '/' => BasePathFs rooted at serverRoot => FullPath spans whole root)
auth/proxy.go:48 and auth/hook.go:163 (same root-resolving default scope on proxy/hook auto-provision)
.dokima/state/dismissed-concerns.jsonl: the two 'Empty Defaults.Scope ... grants scope /' dismissals only computed path.Join('/','') for an admin-CHOSEN empty string and framed it as 'admin misconfiguration'; neither computed that the shipped '.' default resolves identically to '/' with zero scope configuration (material change in reachability)