aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/conftest.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-12 12:14:15 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-12 16:36:54 +0200
commit6260825bf6d8340549de53905de3bd0b84d97d0a (patch)
tree7c2fc96613f3b499c15e9e2728ffaf7afdf4385b /packages/meshbay-hub/tests/conftest.py
parent28548dcbde50f7cee471d6962e16115bb87b056f (diff)
downloadmeshbay-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 'packages/meshbay-hub/tests/conftest.py')
-rw-r--r--packages/meshbay-hub/tests/conftest.py7
1 files changed, 6 insertions, 1 deletions
diff --git a/packages/meshbay-hub/tests/conftest.py b/packages/meshbay-hub/tests/conftest.py
index cb595c5..69c3c5b 100644
--- a/packages/meshbay-hub/tests/conftest.py
+++ b/packages/meshbay-hub/tests/conftest.py
@@ -95,7 +95,12 @@ def _skip_email_verification(monkeypatch):
monkeypatch.setattr(
"meshbay_hub.api.users._create_and_send_verification", _noop)
- monkeypatch.setattr("meshbay_hub.mail._send", lambda msg: True)
+ # `**_` because `_send` takes a required keyword `purpose` — the gate
+ # that keeps the hub off the open-relay list. A stub with the old
+ # signature turns every send into a TypeError, which looks like a bug
+ # in the handler. `test_mail_is_not_a_relay.py` opts out of this and
+ # drives the real thing.
+ monkeypatch.setattr("meshbay_hub.mail._send", lambda msg, **_: True)
@pytest.fixture(autouse=True)