Public share auth treats an empty stored Token as a valid match, bypassing the password on legacy tokenless shares
medium Possibly Valid medium confidence
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
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.
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 equalityshare/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 equalityshare/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