aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_availability_between_members.py
Commit message (Collapse)AuthorAgeFilesLines
* fix(hub): no mail from the event loop, and a ceiling on every path that sends itChristophe Besson2026-09-121-0/+132
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `users.py` and `admin.py` under the availability lens. `admin.py` needed nothing — its moderator/admin line is drawn explicitly, self-modification is refused, and every list it serves is bounded. `users.py` had four findings and one of them is the worst of this whole pass. AV9 `mail._send` is `smtplib` with a ten-second timeout, called straight from four async handlers. That wait is not one request's, it is the instance's: nothing else served, no node socket read, no WebRTC offer relayed, until the MTA answers. Reachable by any signed-in user at request rate through the endpoint below. It has no symptom a test catches — everything simply works slowly, for everyone, whenever the mail server is having a bad day. AV10 `PATCH /v1/users/me` is the third path that makes the hub send mail and the only one with neither a rate limit nor a captcha, while `register` and `password/reset-request` have both. The address is any string the caller types and the duplicate check only rejects one already held by an account here, so every address *not* registered on this hub was a valid target: a relay for verification codes with the hub's own reputation attached. A rate limit counting by IP bounds a caller and not an inbox, so the floor under it is a cooldown per account — the same for a reset request, whose cost also lands in a mailbox that is not the asker's. AV11 `default_tab:` accepted any suffix on a `{key:path}` route with an unbounded Text value and no cap on rows: one account could write without limit into a table shared with everyone. The suffix is a group id, which is what the SPA writes, so it is checked as one. A key over 64 characters was also a 500 rather than a 400 — the column is String(64), which PostgreSQL enforces and SQLite does not, so it would have appeared in production and in no test. AV12 `/v1/notifications` and `/v1/groups` had no upper bound on `limit` and no floor under `offset`, while every list in `admin.py` carries `le=200`. The group directory takes no authentication at all. Two shapes recur and are now named in §13.5b: a limit written on one of several equivalent paths, and a bound that counts the wrong thing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
* fix: bound what one member can cost the othersChristophe Besson2026-09-121-0/+388
An availability review, prompted by the group claim above: a participant supplies input — who else bears the cost? Six answers where the cost fell on someone other than the sender, and none of them needs an attacker. AV3 `chat_notify` carried a `group_id` the hub believed, so any connected node could write a notification to every member of any group on the hub, carrying a display string of its choosing, with its account having no relation to that group. This is the group claim again, two hundred lines further down the same socket. Gated on what the node is registered for, and metered: the fan-out is one write per member. The budget expires by time rather than on disconnect, or reconnecting would refill it and a node token is good for an hour. AV4 A swarm source named its own `endpoint` as free text documented as "ip:port", so an account could publish a third party's address — H6's `peer_ip` defect, never applied here. Nothing dials a swarm source today, which is the only reason it was not already a reflection primitive. It is a transport and a port now, never a host, and the number of hashes one account may claim is bounded: rows were keyed (hash, account) with no cap at all. AV5 `handle_webrtc_answer` resolved any pending `peer_id` from any node's socket. The answer is the SDP a browser then connects to. That this had not happened rested on a uuid4 being unguessable. AV6 `relay_register` had no authentication of any kind: it compared `pk_relay` against the approved value, which is a *public* key, so anyone who could read it could rewrite where the hub tells nodes to send relayed traffic. The module docstring promised signed JWTs and `jwt` was imported and never used. AV7 The node held unlimited peer connections and kept one that never completed a handshake for the life of the daemon. H6 bounded what one unauthenticated peer costs; the hub's cap is three offers in flight per *account*, a limit on each caller and not on the machine, so an operator's exposure grew with the size of their groups. AV8 `invite-notify` put a request-supplied `group_name` into the subject of an email the hub sends under its own domain, to any account, with no rate limit. The name comes from the group row now. The tests are two accounts each, in one file that says why: a one-member test proves a one-member property, and every finding here needed a second person to exist at all. Each was checked against the unfixed code. Two did not survive that check and were rewritten — one re-enacted the disconnect path instead of running it (hence `forget_node`), the other called the reaper itself and would have passed with the call removed from `handle_offer`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT