DOKIMASecurity review report

Overview

Public share auth treats an empty stored Token as a valid match, bypassing the password on legacy tokenless shares

medium Possibly Valid medium confidence

DOK-100085-FILEBROWSER-SHARES-PUBLIC-EMPTY-STORED-TOKEN · filebrowser · shares-public

Status

Possibly Valid

The empty-Token password bypass is a genuine, code-proven auth logic flaw, but it is reachable only against legacy tokenless password-protected shares (current code always co-sets Token and has no update path), so possibly_valid with that data-state precondition named.

Repository / Component

Repository
filebrowser
Component
shares-public

Plain-English Description

A password-protected share whose stored access token is empty can be opened without the password by supplying an empty token in the URL. This affects older shares created before the token feature existed, letting anyone with the share link view protected content without knowing the password.

Description of the Underlying Issue

authenticateShareRequest (http/public.go:129-136) short-circuits to allow access when r.URL.Query().Get("token") == l.Token (line 134), with no guard that l.Token != "" and using non-constant-time string equality. The Token field was added later to share.Link (share/share.go, documented with omitempty), and the bolt storage layer performs no schema migration or backfill (storage/bolt/share.go:37-45,67-69) — a stored record that predates the field unmarshals to Token == "". For such a record, an attacker supplying an empty token (?token= or no non-empty value) matches the empty stored Token and is granted access at line 134/135, before the password check at lines 138-144 is ever reached. The current sole writer, sharePostHandler (http/share.go:148-163), always co-sets Token together with PasswordHash (only when len(hash) > 0) and there is no PATCH/update path, so no newly created share can enter the empty-Token-with-password state.

Potential Attack

An attacker who knows or guesses the hash/URL of a legacy password-protected share (one created before the Token field existed, so PasswordHash is set but Token is empty in the database) requests it with an empty token parameter. The equality check r.URL.Query().Get("token") == l.Token evaluates "" == "" as true and returns success, so the handler never prompts for or validates the share password.

Outcomes of Potential Attack

Unauthorized access to the contents of a password-protected share (file listing/download) without the password, for any affected legacy share. The non-constant-time comparison is a secondary weakness but the empty-Token case is a direct authentication bypass for the affected data state.

Affected Scope

Public share auth: a legacy password-protected share whose stored Token is empty is bypassable via ?token= (empty), skipping the password.

Suggested Fix (plain english)

Do not accept an empty token as a valid match.

Suggested Fix (detailed)

In authenticateShareRequest (http/public.go), add an explicit guard so the token branch is only taken when l.Token != "" (e.g., require l.Token != "" && subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("token")), []byte(l.Token)) == 1), falling through to the password check otherwise. Additionally add a one-time migration/backfill in the bolt storage layer (storage/bolt/share.go) to populate Token for any legacy password-protected records that lack it. Verify by creating a record with PasswordHash set and Token empty and confirming that an empty ?token= no longer grants access and the password is still required. Add an explicit l.Token != "" guard before the token comparison, use constant-time comparison, and/or backfill/migrate legacy share records to populate Token.

Validation

The empty-Token password bypass is a genuine, code-proven auth logic flaw, but it is reachable only against legacy tokenless password-protected shares (current code always co-sets Token and has no update path), so possibly_valid with that data-state precondition named.

first_opinion

Full Evidence

http/public.go:130-134 if l.PasswordHash=="" return 0; then if r.URL.Query().Get('token')==l.Token return 0 — no l.Token!="" guard, non-constant-time equality
share/share.go Token documented as a later-added field with omitempty
storage/bolt/share.go:37-45,67-69 GetByHash/Save perform no schema migration/backfill; a record lacking Token unmarshals to Token==""
http/share.go:148-163 sharePostHandler is the sole writer and sets Token only when len(hash)>0 (co-set with PasswordHash), with no PATCH/update path
Proven fact: authenticateShareRequest treats an empty stored Token as a valid match for an empty ?token= query, bypassing the password check — proven in public.go
Proven fact: Current share-creation code always co-sets Token with PasswordHash and has no update path, so no NEW share can enter the empty-Token state
Unvalidated fact: Reachability requires a pre-existing legacy password-protected share created before the Token field existed (PasswordHash set, Token empty) — a data-state fact not settleable from source
http/public.go:129-136 — authenticateShareRequest: after the PasswordHash=="" early return, if r.URL.Query().Get("token") == l.Token returns 0 (granted); no l.Token != "" guard, non-constant-time equality (verified in source)
share/share.go — Token is a later-added field carrying omitempty (documented)
storage/bolt/share.go:37-45,67-69 — GetByHash/Save perform no schema migration/backfill, so a record lacking Token unmarshals to Token == ""
http/share.go:148-163 — sharePostHandler is the sole writer and sets Token only when len(hash) > 0 (co-set with PasswordHash); there is no PATCH/update path
Proven fact: authenticateShareRequest treats an empty stored Token as a valid match for an empty ?token= query and grants access before the password check — proven in http/public.go:129-136
Proven fact: Current share-creation code always co-sets Token with PasswordHash and provides no update path, so no newly created share can enter the empty-Token-with-password state
Proven fact: The storage layer performs no migration/backfill of the Token field
Unvalidated fact: Reachability requires a pre-existing legacy password-protected share created before the Token field existed (PasswordHash set, Token empty) — a database data-state fact that cannot be settled from source
http/public.go:130-134 — `if l.PasswordHash == "" { return 0, nil }` then `if r.URL.Query().Get("token") == l.Token { return 0, nil }`; no `l.Token != ""` guard and non-constant-time equality
share/share.go:16-19 — Token documented as a later-added field only set alongside PasswordHash; JSON omitempty
storage/bolt/share.go:37-45,67-69 — GetByHash/Save perform no schema migration or Token backfill; a stored record lacking the token field unmarshals to Token==""
http/share.go:142-163 — sharePostHandler is the sole writer and sets Token only when len(hash)>0, confirming current-code shares are consistent and the empty-Token state is legacy-only