Gate the chat's websocket, and make a captcha pass expire (review, area 1b) - #305
Merged
Merged
Conversation
From the review of the public surface (area 1b). Reproduced first, then fixed, then checked in a real browser through Turnstile's test keys. - The captcha gate was HTTP middleware, which never sees a websocket. A client opening the chat's socket.io websocket directly got the whole chat with no captcha. An ASGI middleware now refuses ungated websocket handshakes (close 1008, a 403 to the client). - Guests' only per-person limit was per session, and the client picks the session id. Guests are now also limited per captcha solve (100 messages per 3h, configurable), which a client cannot reset without solving again. - The cookie was value|HMAC(value) with no issue time: one solve was a pass for ever, for anyone. It now carries an issue time and a nonce, expires on the server after 12h, and renews while in use so an open tab keeps working. Old-format cookies are refused: readers solve once more. - verify_captcha called the blocking requests.post on the shared event loop for every anonymous POST, and a non-JSON reply was a 500. Now async httpx, failure treated as not verified, and the form is bounded (no files) -- it spooled unbounded uploads to the shared disk. - The Referer check returned 400 to readers arriving from any http page and stopped no one. Removed. - The API exemption is the prefix with its slash; /chat/guest/api-x was exempt too. tests/api/test_captcha_gate.py drives the real app over HTTP and websocket in its own process; with the websocket gate removed, its three websocket tests fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
These fixes come from the max-level review of the rest of the public surface. Each problem was reproduced before it was fixed.
@app.middleware("http")doesn't see websocket connections, so a client that opened the socket directly got the full chat with no captcha.WebsocketGate, an ASGI middleware that refuses the handshake (close 1008, which the client sees as a 403) unless there is a current pass.CHAT_MESSAGES_PER_SOLVE(default 100) perCHAT_SOLVE_WINDOW_SECONDS(default 3h).value|HMAC(value)with no issue time.util/captcha_cookie.py:v2.<issued>.<nonce>|HMAC. It expires on the server after 12h and is renewed after 30 min of use. Old-format cookies are refused.requests.postwas blocking, and a non-JSON reply was a 500. Uploads to the check form were unbounded and spooled to disk.httpx, with any failure treated as "not verified".request.form(max_files=0, max_fields=4, max_part_size=8192).Secure./chat/guest/api-anything.Tests
tests/api/test_captcha_gate.pydrives the real app over HTTP and websocket, in its own process.tests/util/test_captcha_cookie.pycovers expiry, renewal, nonces, tampering, the old format, and future timestamps.Verified in a real browser
I ran this through Turnstile's always-pass test keys over HTTPS (
~/chat-uitest/gate_browser.py):v2.cookie is set.handoff_gate.py) passes: first visit, reload, returning visitor.Worth knowing
🤖 Generated with Claude Code