diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 12:14:15 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | 6260825bf6d8340549de53905de3bd0b84d97d0a (patch) | |
| tree | 7c2fc96613f3b499c15e9e2728ffaf7afdf4385b /CLAUDE.md | |
| parent | 28548dcbde50f7cee471d6962e16115bb87b056f (diff) | |
| download | meshbay-6260825bf6d8340549de53905de3bd0b84d97d0a.tar.gz | |
fix(hub): the mail server is not a relay
The previous commit metered the paths that send mail. It was not enough, and
saying it was would have been wrong: a 60-second cooldown per account still
allows one stranger a minute — 1440 a day — and registration is open, so
"per account" is a bound an attacker buys more of. And there was a third door
nobody had counted.
POST /v1/users/register an address nobody has verified
PATCH /v1/users/me an address nobody has verified, signed in
POST /v1/users/password/reset only the address already on file
POST /v1/groups/{id}/invite-notify only a registered member's address
The widest was the register *resend* branch: no token, no captcha, and the
username and address are the caller's own from a moment ago — registering a
victim's address once bought the right to mail them at the endpoint's rate
limit for as long as the account stayed pending.
So the bound moves into `mail.py`, where 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 rather than an unrestricted send. Under it:
- a bound per **recipient**, across every purpose, account and endpoint —
what a person being mail-bombed actually experiences, and the only bound
that describes it. Keyed on a hash, because this would otherwise be the
one place in the hub holding plaintext addresses in memory (S2)
- an instance-wide hourly ceiling, which cannot be bought with more accounts
- a cooldown on the resend branch, a cooldown and a daily ceiling on the
address change, and the IP-log entry that endpoint never wrote — alone
among the ones that mail
The ceiling on address changes counts IP-log rows, not EmailVerification: the
handler deletes this account's unverified rows before writing a new one, so
counting those counts one, always. Which is what the first version of it did.
Refusals never carry the address: that line goes to the journal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'CLAUDE.md')
0 files changed, 0 insertions, 0 deletions