diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 12:14:40 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 17c06bc23252929af13f4361c4a6300ee76a0c51 (patch) | |
| tree | 0f6cc4e0e3680ac253a88ccad580e97019abf77f /docs/MESHBAY_DESIGN.md | |
| parent | c8462c6060ccd2cccb305edcd64ef12a675f2f58 (diff) | |
| download | meshbay-17c06bc23252929af13f4361c4a6300ee76a0c51.tar.gz | |
docs: AV13–AV15, and two lessons from how they were found
AV13 the mail gate, AV14 the MHP audience and the hub's own identity, AV15
the shape check on the unauthenticated blocklist endpoints. §13.5b also
records the auto-block threshold as an open decision rather than a defect.
Two lessons, both about tests.
A fixture narrower than production tests the fixture — and when it writes down
*why* it has to be narrow, that is a bug report nobody filed. Two test helpers
described the federation defects accurately, in comments, while routing
around them.
And a global's state is not a given, in a test least of all. One assertion —
that a refusal never logs the address — took three attempts, each passing
alone and failing in the full run, for three different reasons: caplog saw
nothing because the app configures logging, then a handler saw nothing
because an earlier test raised the logger's level, then because `disabled`
was left True.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'docs/MESHBAY_DESIGN.md')
| -rw-r--r-- | docs/MESHBAY_DESIGN.md | 17 |
1 files changed, 15 insertions, 2 deletions
diff --git a/docs/MESHBAY_DESIGN.md b/docs/MESHBAY_DESIGN.md index 16e728f..8da4552 100644 --- a/docs/MESHBAY_DESIGN.md +++ b/docs/MESHBAY_DESIGN.md @@ -2499,6 +2499,16 @@ the wrong thing** — a per-IP rate limit bounds a caller, never the mailbox tha receives what they cause, which is why `AV10` is a cooldown per *account* under a limit per IP rather than a tighter limit. +**Open for decision, not a defect:** `moderation.AUTO_BLOCK_THRESHOLD` is 3. +Three distinct accounts blocking a hash adds it to the list every node +enforces, network-wide, automatically, with manual admin removal the only +undo. That is already far better than the anonymous version it replaced, and +it is still a censorship primitive an attacker buys for the price of three +email addresses. Raising it buys little; requiring the reporting accounts to +be more than a day old would cost a patient attacker a day and cost an honest +reporter nothing after their first. Left as it is because it is a moderation +policy rather than a bug, and the person who sets that policy is the operator. + `admin.py` was read under this lens and needed nothing. Its moderator/admin line is drawn explicitly — a moderator may not change a role, may not revoke, and may not touch an admin's account at all — self-modification is refused, and @@ -2516,9 +2526,12 @@ had already been asked. | **AV7** | **A node bounds how many peers it holds and how long an unproven one lasts.** The hub's cap is per calling account, which is a limit on each member and not on the machine, so without this an operator's exposure grew with the size of their groups | | **AV8** | **One account cannot make the hub mail another at will.** The invitation email's subject comes from the group row, never from the request, and the endpoint is metered | | **AV9** | **No mail is sent from the event loop.** `smtplib` is synchronous and waits up to ten seconds; called from an async handler that wait is the whole instance's, not one request's. Every send goes through `mail.send_off_loop` | -| **AV10** | **Every path that makes the hub send mail is metered, per account.** A rate limit that counts by IP bounds a caller, not an inbox. Changing one's address mails an arbitrary stranger, so it carries a cooldown as well as a limit; so does a reset request | +| **AV10** | **Every path that makes the hub send mail is metered, per account.** A rate limit that counts by IP bounds a caller, not an inbox. Changing one's address mails an arbitrary stranger, so it carries a cooldown *and* a daily ceiling; a reset request and a registration resend carry cooldowns | +| **AV13** | **The mail server is not a relay, and `mail.py` is where that is decided.** Every message passes one function; `purpose` is keyword-required and checked against a closed list, so a helper that names anything else does not send and one that names nothing is a TypeError. Under it sit a bound per **recipient** — the thing a person being mail-bombed actually experiences, unmoved by which account, address or endpoint asks — and an instance-wide hourly ceiling, because registration is open and "per account" is a bound an attacker buys more of | | **AV11** | **A namespace a client writes into is closed, and its rows are capped.** The preference key space is an allow-list plus `default_tab:<group_id>` checked as a group id, the value is length-bounded, and the row count per account is bounded | -| **AV12** | **Every list has an upper bound on `limit` and a floor under `offset`.** Including the ones that take no authentication at all | +| **AV12** | **Every list has an upper bound on `limit` and a floor under `offset`.** Including the ones that take no authentication at all — the public group directory and the content blocklist | +| **AV14** | **MHP binds its audience, and the hub reads its own identity at call time.** A token is minted for one peer and accepted by that peer only. `federation.py` bound `_hub_id` and `_hub_sk_pem` at import, which is before `load_hub_keypair` runs, so it signed with `None` and called itself `meshbay.org` whatever the instance was named — and the verifier named no audience for the `aud` the issuer sets, which PyJWT refuses outright. MHP could not complete one authenticated request between two hubs | +| **AV15** | **A hash is checked for shape before it is a key lookup**, on the unauthenticated blocklist endpoints a node consults | ### 13.6 Chat design findings |