DOKIMASecurity review report

Overview

Login reCAPTCHA verification checks only the success flag, allowing cross-origin token replay

low Possibly Valid low confidence

DOK-100102-FILEBROWSER-AUTH-METHODS-RECAPTCHA-LOGIN-VE · filebrowser · auth-methods

Status

Possibly Valid

Proven that verification checks only success; the v2 hostname-replay gap is a genuine defense-in-depth weakness in an admin-enabled control, so not rejected. But the headline that the control 'does not block automated logins' is overstated for the default v2 frontend (v3 score facet unreachable; v2 still imposes a per-token solve cost), and exploitability hinges on deployment/attacker preconditions not settleable from source — hence possibly_valid at low severity and low confidence. Canonical of the 100102/100105 pair; distinct weakness from the reCAPTCHA-timeout DOK-100000.

Repository / Component

Repository
filebrowser
Component
auth-methods

Plain-English Description

When the optional login CAPTCHA is turned on, the server only trusts that Google said the puzzle was solved, but never checks that it was solved on the real site. An attacker could collect solved puzzles from a page they control and reuse them to get past the CAPTCHA on the login form.

Description of the Underlying Issue

In auth/json.go the reCAPTCHA verification (ReCaptcha.Ok, lines 91-121) POSTs the secret and user response to the siteverify endpoint and decodes the reply into a struct that contains only the `success` boolean. It returns `data.Success` without inspecting the response `hostname` (the origin where the token was solved), the v3 `score`, `action`, or `challenge_ts`. The sole caller, JSONAuth.Auth (lines 45-56), treats Ok()==true as CAPTCHA passed and is gated only on Secret being non-empty. Because the origin/hostname bound to each token is never validated, a token solved for the same public site key on a different origin is indistinguishable from a legitimate one.

Potential Attack

With v2 reCAPTCHA enabled, an attacker embeds the deployment's public site key on an origin they control (or uses a solving service), harvests solved response tokens there, and replays each single-use token against POST /api/login. Because Ok never compares the response hostname to the server's own host, the origin mismatch is not detected and the CAPTCHA gate passes, letting the attacker continue automated credential-guessing behind a control meant to throttle it.

Outcomes of Potential Attack

The attacker neutralizes the anti-automation value of the CAPTCHA, restoring the ability to script login attempts (credential stuffing / brute force) against the login endpoint. It does not by itself grant authentication — valid credentials are still required — so the impact is degraded defense-in-depth on the login throttle rather than a direct authentication bypass.

Affected Scope

Server-side reCAPTCHA verification in auth/json.go ReCaptcha.Ok, reached from POST /api/login when json auth + reCAPTCHA are enabled.

Suggested Fix (plain english)

When verifying a solved CAPTCHA, also confirm it was solved on your own site's address, not just that it was solved somewhere.

Suggested Fix (detailed)

In ReCaptcha.Ok, decode the full siteverify response and validate it: reject when the returned `hostname` does not match the server's expected host, and (if v3 support is ever added to the frontend) enforce a configurable minimum `score` and optionally `action`. Expose expected-hostname and min-score as settings fields. Verification check: confirm a token solved for the same site key on a foreign origin is rejected at ReCaptcha.Ok. In ReCaptcha.Ok, decode and validate the full siteverify response: reject when hostname does not match the server's expected host (and, if v3 support is ever added to the frontend, enforce a configurable score threshold and optionally action). Expose expected-hostname/min-score as settings.

Validation

Proven that verification checks only success; the v2 hostname-replay gap is a genuine defense-in-depth weakness in an admin-enabled control, so not rejected. But the headline that the control 'does not block automated logins' is overstated for the default v2 frontend (v3 score facet unreachable; v2 still imposes a per-token solve cost), and exploitability hinges on deployment/attacker preconditions not settleable from source — hence possibly_valid at low severity and low confidence. Canonical of the 100102/100105 pair; distinct weakness from the reCAPTCHA-timeout DOK-100000.

first_opinion

Full Evidence

auth/json.go:112-121 — the siteverify response is decoded into struct { Success bool `json:success` } and Ok returns data.Success with no inspection of hostname, score, challenge_ts, or action
auth/json.go:91-107 — Ok only POSTs secret+response to Host+reCaptchaAPI; no remoteip and no score threshold are configurable
auth/json.go:45-56 — sole caller JSONAuth.Auth treats Ok()==true as passed, gated only on Secret != ''
frontend/src/views/Login.vue:133-136 and :86 — the frontend uses window.grecaptcha.render(...) and grecaptcha.getResponse(), i.e. the reCAPTCHA v2 widget API; frontend/public/index.html:12 loads api.js?render=explicit (v2), with no grecaptcha.execute(...) v3 path anywhere
settings/ has no reCAPTCHA score-threshold or expected-hostname field (config lives only in auth.JSONAuth.ReCaptcha)
Proven fact: Verification checks only the success boolean; the response hostname (origin where the token was solved) and any v3 score are never validated
Proven fact: The shipped frontend implements reCAPTCHA v2 only, so with the default frontend the missing check reduces to: no hostname validation on v2 tokens (enabling cross-origin token replay using the public site key)
Unvalidated fact: The v3-score facet of the claim is NOT reachable with the shipped v2-only frontend; it would require an administrator to replace the frontend to emit v3 tokens — a non-default modification not evidenced in the repo
Unvalidated fact: The v2 hostname-replay attack requires the attacker to harvest solved tokens for the public site key from an origin they control (solving service or third-party visitors) — an external capability not settleable from source
Unvalidated fact: reCAPTCHA is opt-in and off by default; the residual exposure does not exceed the project's already-accepted no-login-rate-limit baseline, so the practical benefit of the missing checks is marginal
auth/json.go:112-121 — siteverify response decoded into `struct { Success bool `json:"success"` }`; Ok returns data.Success with no hostname/score/action/challenge_ts inspection
auth/json.go:91-107 — Ok only POSTs secret+response to Host+reCaptchaAPI; no remoteip sent and no score threshold configurable
auth/json.go:45-56 — JSONAuth.Auth treats Ok()==true as passed, gated only on Secret != ''
frontend/src/views/Login.vue:133-136,86 — frontend uses grecaptcha.render/getResponse (v2 widget API); frontend/public/index.html:12 loads api.js?render=explicit (v2), no grecaptcha.execute (v3) path anywhere
settings/ has no reCAPTCHA score-threshold or expected-hostname field; config lives only in auth.JSONAuth.ReCaptcha
Proven fact: Verification checks only the success boolean; the response hostname and any v3 score are never validated (auth/json.go:112-121).
Proven fact: The shipped frontend implements reCAPTCHA v2 only, so with the default frontend the gap reduces to: no hostname validation on v2 tokens, enabling cross-origin token replay using the public site key.
Unvalidated fact: The v3-score facet is not reachable with the shipped v2-only frontend; it would require an administrator to replace the frontend to emit v3 tokens — a non-default modification not evidenced in the repo.
Unvalidated fact: The v2 hostname-replay attack requires the attacker to harvest solved tokens for the public site key from an origin they control — an external capability not settleable from source.
Unvalidated fact: reCAPTCHA is opt-in and off by default; residual exposure does not exceed the project's already-accepted no-login-rate-limit baseline, so the practical benefit of the missing checks is marginal.
auth/json.go:110-121 — response decoded as `var data struct{ Success bool `json:"success"` }`; function returns data.Success with no hostname/score/challenge_ts/action inspection
auth/json.go:91-107 — Ok() only POSTs secret+response to Host+reCaptchaAPI; no remoteip sent and no score threshold configurable
auth/json.go:45-56 — the only caller: JSONAuth.Auth treats Ok()==true as 'passed', gated on Secret != ""
repo-wide grep for `hostname|score|challenge_ts|action|siteverify` in *.go → 0 matches (no other code compensates)
settings/ has no reCAPTCHA score-threshold or expected-hostname configuration field (grep for `recaptcha` in settings/ → 0 hits; config lives only in auth.JSONAuth.ReCaptcha)