From 7d8c774e250b134222374f45726d5426714475de Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Tue, 1 Sep 2026 17:49:13 +0200 Subject: fix(hub): enforce registration captcha for every client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server only checked the captcha when auth_key was absent — but every real client (browser included, via the password split) sends auth_key, so the check was off for everyone, and a bot skipped it by including the field. The Register form still made humans solve a widget whose token was never transmitted. Gate is now unconditional on captcha.enabled. The web client (registerUser in keyderive.js) forwards captcha.token; RegisterPage resets the (single-use) token on a failed attempt. The desktop client shares this UI source and is Chromium, so it renders the same widget (see the paired meshbay-client commit for the CSP change that allows it). Tests: test_register_captcha.py. Third security review, finding M1 (Option A). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011pG75yGK3NthNfyjH74omG --- .../meshbay-hub/tests/test_register_captcha.py | 58 ++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/meshbay-hub/tests/test_register_captcha.py (limited to 'packages/meshbay-hub/tests/test_register_captcha.py') diff --git a/packages/meshbay-hub/tests/test_register_captcha.py b/packages/meshbay-hub/tests/test_register_captcha.py new file mode 100644 index 0000000..befc1e2 --- /dev/null +++ b/packages/meshbay-hub/tests/test_register_captcha.py @@ -0,0 +1,58 @@ +"""Registration CAPTCHA is enforced for every fresh account when configured. + +The gate used to be skipped whenever the request carried an `auth_key` — which +every real client sends (the password split) — so it protected nobody and a bot +skipped it by including the field. It now runs on `captcha.enabled` alone; the +desktop client is Chromium and renders the same widget. +""" + +import pytest + + +@pytest.fixture +def captcha_on(client, monkeypatch): + """Turn on a fake captcha: any config with both keys is `enabled`, and + verification succeeds only for the token 'good-token'.""" + from meshbay_hub.api.users import _cfg + monkeypatch.setattr(_cfg.captcha, "site_key", "test-site") + monkeypatch.setattr(_cfg.captcha, "secret_key", "test-secret") + + async def fake_verify(secret, token, remote_ip=None): + return token == "good-token" + + monkeypatch.setattr("meshbay_hub.captcha.verify_captcha", fake_verify) + + +def _body(**over): + b = {"username": "newbie", "email": "newbie@t.com", "auth_key": "a" * 44} + b.update(over) + return b + + +@pytest.mark.asyncio +async def test_missing_captcha_rejected_even_with_auth_key(client, captcha_on): + r = await client.post("/v1/users/register", json=_body()) + assert r.status_code == 400 + assert r.json()["detail"] == "captcha_required" + + +@pytest.mark.asyncio +async def test_bad_captcha_rejected(client, captcha_on): + r = await client.post("/v1/users/register", + json=_body(captcha_token="wrong")) + assert r.status_code == 400 + assert r.json()["detail"] == "captcha_failed" + + +@pytest.mark.asyncio +async def test_good_captcha_accepted(client, captcha_on): + r = await client.post("/v1/users/register", + json=_body(captcha_token="good-token")) + assert r.status_code == 201 + + +@pytest.mark.asyncio +async def test_no_captcha_configured_still_registers(client): + # Default test config has no captcha keys — registration proceeds without one. + r = await client.post("/v1/users/register", json=_body()) + assert r.status_code == 201 -- cgit v1.2.3