diff options
Diffstat (limited to 'docs/captcha.md')
| -rw-r--r-- | docs/captcha.md | 110 |
1 files changed, 71 insertions, 39 deletions
diff --git a/docs/captcha.md b/docs/captcha.md index c908a5c..84dadc4 100644 --- a/docs/captcha.md +++ b/docs/captcha.md @@ -12,7 +12,9 @@ reCAPTCHA v2 with the "I'm not a robot" checkbox. Reasons: - Binary pass/fail — no score threshold to tune or monitor. - The user is already filling a form; one checkbox is negligible friction. -- Works in both the web SPA and the Electron client (both run Chromium). +- Works in the web SPA. It works in the Electron client too, but not for the + reason "both run Chromium" — reCAPTCHA validates the *domain*, not the + rendering engine, and the desktop client's is not the hub's. See §6. - v3 (invisible, score-based) is an option later if the checkbox proves annoying; the server-side verification call is identical, only the client widget differs. @@ -20,6 +22,9 @@ reCAPTCHA v2 with the "I'm not a robot" checkbox. Reasons: `https://www.google.com/recaptcha/admin`. Register the hub's domain(s) — `meshbay.org` and `localhost` for development. This produces a **site key** (public, embedded in HTML) and a **secret key** (server-only, in `hub.toml`). +If the desktop client is in use, also turn *off* "Verify the origin of +reCAPTCHA solutions" on that key and set `allowed_hosts` — §6 says why, and +what is given up. --- @@ -376,49 +381,69 @@ captcha.failed: "Captcha verification failed — please try again" --- -## 6. Desktop client (Electron) +## 6. Desktop client (Electron), and the domain problem -The Electron client loads its UI from the package (`app://meshbay`), not from -the hub. Its CSP forbids external scripts by design — the renderer executes no -code that did not ship in the package. Loading Google's reCAPTCHA script -(`https://www.google.com/recaptcha/api.js`) would be a **new category of -trust**: third-party executable code in a renderer that currently runs none. +**This section replaced two earlier designs, and both are worth naming because +the reasoning that produced them is the trap.** -Two approaches: +The first said: open the CSP to Google's reCAPTCHA domains, or skip the captcha +for native clients — and recommended skipping it, keyed on `auth_key` being +present. That carve-out shipped and was a hole: *every* real client sends +`auth_key`, the browser included (it is the password split), so the gate was +off for everybody and a bot skipped it by including the field. It is gone; +`users.py` gates on `captcha.enabled` alone, and says so at the call site. -- **Open the CSP to Google's reCAPTCHA domains.** Add - `https://www.google.com/recaptcha/` and `https://www.gstatic.com/recaptcha/` - to `script-src`, `frame-src` and `connect-src`. Functional, but undermines - the principle that the renderer runs only packaged code — the reCAPTCHA - script is fetched live and changes without the operator's knowledge. -- **Skip the captcha for native clients** (recommended). The Electron client - already raises the bar against automated account creation: it requires - installation, generates a device Ed25519 key pair stored in `safeStorage`, - and authenticates to the hub via `POST /v1/users/auth` with a signed - challenge. A bot automating that path must install and drive a full Electron - app, which is a harder problem than filling a web form — and the reCAPTCHA - exists to solve the web-form problem. +The second is the sentence in §1 above: "works in the Electron client too, both +run Chromium". The CSP was opened (`RECAPTCHA_SRC` in `main.js`, covering +`script-src`, `img-src` and `frame-src`) and the widget does render. It renders +**"ERROR for site owner: Invalid domain for site key"**. -**How to skip server-side.** The web SPA sends `password` (legacy) in the -registration body; the Electron client sends `auth_key` (PBKDF2-derived via -`window.MeshBayKeys.registerUser`). The server requires `captcha_token` only -when `auth_key` is absent — i.e. the web path. This is not a security -boundary: a bot that derives `auth_key` itself bypasses the check, but it also -proves it can run the PBKDF2 derivation, which is the same cost as solving the -captcha. The real gate for native-path abuse is the rate limiter (`5/minute`) -and the email verification step. +**Why.** reCAPTCHA validates the hostname of the page the widget is rendered +in, against the domain list on the site key. The desktop client's interface +ships inside the package and is served from `app://meshbay` (`main.js`: +`win.loadURL('app://meshbay/index.html')`). Not a preference — `file://` +breaks ES modules and IndexedDB, and the hub must never become the document +origin, which is enforced by the `will-navigate` handler. So the hostname +Google sees is `meshbay`, it is not on the key's list, and it never can be: +the check happens on Google's servers and no client-side configuration reaches +it. Widening the CSP does not help, because the CSP was never what refused. -```python -# In register(), captcha gate adjusted: -if _cfg and _cfg.captcha.enabled and not body.auth_key: - if not body.captcha_token: - raise HTTPException(400, "captcha_required") - ... +**What is done instead.** Turn *off* "Verify the origin of reCAPTCHA +solutions" on the key, and check the origin on the hub, where it belongs: + +```toml +[captcha] +site_key = "6Le..." +secret_key = "6Le..." +allowed_hosts = ["meshbay.org", "localhost", "meshbay"] ``` -On the Electron side: `registerUser()` in `keyderive.js` does not send -`captcha_token`, and the server does not ask for one. No CSP change, no -Google script loaded, no new trust boundary. +`verify_captcha` then refuses a solve whose reported hostname is not in that +list. The hostname comes from `siteverify` — it is what Google *observed*, not +something the caller asserts — so this is a real check and not a formality: the +site key is public, and the thing turning the origin check off opens is a bot +rendering the widget on a page of its own, which this refuses on the hostname +it actually served from. + +Empty (the default) means "do not check", so a hub that never touched this +setting keeps the behaviour it has, with reCAPTCHA doing the origin check +itself. **The two settings go together**: turning the console check off without +setting `allowed_hosts` leaves no origin check anywhere. + +**The last entry is the desktop client's own, and it is the weak one.** Any +Electron application can claim the same scheme and host — `main.js` already +records that `app://meshbay` is not a credential, which is why the hub's API is +reachable from no web origin at all and every call leaves from the main +process. So `meshbay` in that list is spoofable by someone who builds an +equivalent application. What it still costs them is a per-token captcha solve +inside a real Chromium, rather than a token farmed from any web page. That is +the trade, stated plainly; a hub that does not ship the desktop client should +leave the entry out. + +**Confirming the hostname.** `meshbay` is the host component of +`app://meshbay`. If a solve is refused, `captcha.py` logs it at WARNING with +the hostname spelled out and the allowed list beside it, which is how to read +the value a given client actually reports rather than guess at it. --- @@ -475,11 +500,15 @@ from Google — no npm package. ## 10. Deployment steps 1. Obtain reCAPTCHA v2 keys from Google (register `meshbay.org` + `localhost`). + For a deployment with the desktop client, also turn off "Verify the origin + of reCAPTCHA solutions" on the key — §6. 2. Add to `/etc/meshbay/hub.toml` on the production server: ```toml [captcha] - site_key = "6Le..." - secret_key = "6Le..." + site_key = "6Le..." + secret_key = "6Le..." + # Required whenever the console's origin check is off, and only then. + allowed_hosts = ["meshbay.org", "localhost", "meshbay"] ``` 3. Deploy the new hub code (`deploy-hub.sh` — runs `alembic upgrade head` + restart; no migration needed for this change). @@ -488,3 +517,6 @@ from Google — no npm package. solving the captcha. 5. Verify reset: open `https://meshbay.org/#/reset`, confirm the checkbox appears. Request a reset code, confirm email arrives only after solving it. +6. Verify the desktop client: register from it and confirm the widget solves + rather than showing "Invalid domain for site key". A refusal logged as + `captcha solved on an unexpected host` names the hostname to add. |