carlos / platform Public

Mail: a member can adopt another party's verified SES domain; sending-domain claims never expire Issue open

On branch tenant-iso-mail

Claimed by claude~paul@keymail.dev 3 weeks ago

Found by the 2026-10-03 tenant-isolation audit of main 78fc8d40, by reading code.

  1. High: a member can take over a sending domain already verified in SES outside CARLOS. - POST .../email/domains (console/apimail.go:1016) is a member verb. It calls mail.EnsureDomain โ†’ claimDomain, which is first-come (mail/domainclaim.go:119) โ†’ EnsureIdentity. - On AlreadyExists, EnsureIdentity adopts the identity and its DKIM tokens (mail/aws.go:306-317) and overwrites MAIL FROM (:330). - Examples of exposed identities: tito.io (spec 2026-09-15 ยง1), the operator's own sign-in mail domain, and anything verified before the claim gate. - VerifyDomain reads Verified at once, so A's IAM policy then allows sending From *@thatdomain. It also rebinds the identity's configuration set to A's, and A's later Teardown deletes the identity, which breaks the real owner's mail.
  2. Sending-domain claims never expire and need no proof (mail/domainclaim.go:119-141, console/apimail.go:994-1031). Any account Admin can claim victim.com and block every other tenant from it forever, and the refusal "already claimed by another CARLOS account" confirms someone holds it.
  3. All tenants share one SES sending reputation. A credential cannot be forced to name a configuration set, and caps and pauses act on events after the fact. One abusive tenant risks an account-level SES suspension that stops mail for everyone.

Fixes for 1 and 2 are going on a branch from this session. 3 needs a design decision (a dedicated SES account per tenant, or enforcing the configuration set on every send), so it stays open here.

Items 1 and 2 merged to main via tenant-iso-mail (Fable review plus one re-check, CI green). Item 3, the shared SES reputation, stays open. The cheap option the fix agent found: SES tenants, with ses:TenantName enforced in the IAM policy, so each send names its tenant and a bad tenant is paused on its own.

Tracked under #247 (tenant isolation before Carloku takes outside tenants).