Skip to content

Sign-up has no bot protection — the form is being used to send confirmation email to non-consenting third parties (email bombing) #353

Description

@TortoiseWolfe

Summary

The public sign-up form has no bot protection (no CAPTCHA, no meaningful signup rate limit), and prod data shows it is being used to send confirmation email to people who never requested an account — the signature of subscription / email bombing, where an open signup form is used as a free relay against third parties.

Our domain and our Resend account are the sender, so the exposure is ours: deliverability damage, spam complaints, and possible sender-account suspension.

Note: deliberately no email addresses in this issue — the affected parties are non-consenting third parties and this repo is public. Details are aggregated. The raw list is available to the maintainer via the Supabase admin API.

Evidence (prod, as of 2026-07-25)

17 non-owner, non-test accounts, all created in a 7-day window: 2026-07-19 → 2026-07-25. That window starts ~2 days after Resend SMTP went live (#290) and sign-up actually began working (#288) — i.e. the abuse began as soon as the path became functional.

Signal Count
Created in the 7-day window 17
Never confirmed AND never signed in 13
Confirmed + signed in (possibly genuine) 4 (one via Google OAuth)

For those 13, our server sent a confirmation email to a real mailbox whose owner never asked for it.

Address composition (redacted to category):

  • A US county-government domain, a Fortune-500 energy company, a law firm, an international shipping/chartering firm, a design agency, a German business domain — unrelated industries and geographies, which is not an organic signup pattern.
  • One carrier email-to-SMS gateway address. Signing that up sends a text message to a phone number — a strong bombing indicator, since the payload is a text, not an email.
  • Six dotted-Gmail aliases (f.i.r.s.t.l.a.s.t@gmail.com-style dot insertion) — classic alias multiplication / obfuscation.

Root cause

scripts/supabase/auth-config.json manages only six keys:

hook_custom_access_token_enabled, hook_custom_access_token_uri,
jwt_exp, rate_limit_verify, site_url, uri_allow_list

There is no CAPTCHA setting and no signup rate limit. Note this also means the auth-config drift gate shipped in #350 will not catch it — the gate only compares keys that are present in the tracked config, so an unset protection is invisible to it.

Proposed fix

  • Enable CAPTCHA on Supabase Auth (hCaptcha or Cloudflare Turnstile) — the actual mitigation. Wire the provider token into the sign-up form.

  • Track the CAPTCHA + signup-rate-limit keys in auth-config.json so the ci(#288): wire the auth-config drift gate into CI — closes #288 #350 drift gate guards them from silently regressing. (Generalises: the gate protects only what it knows about — worth auditing which other security-relevant keys are untracked.)

  • Decide on the 13 unconfirmed accounts. WITHDRAWN 2026-07-25 — do not delete. Investigate deliverability first: Sign-up confirmation email deliverability is unverified — DMARC has no reporting, and dead Mailgun SPF/MX remain after the Resend cutover #356.

    The original read ("bot-generated, never consented, delete them") was overconfident. It rested entirely on how the addresses look — dotted-Gmail aliases, unrelated corporate domains, one carrier email-to-SMS gateway. The evidence that would actually settle it does not exist: auth.audit_log_entries has 0 rows, so there are no IPs, user-agents or timings to distinguish a script from a person.

    A plausible alternative fits the data at least as well: Resend SMTP went live 2026-07-17 and the first of these signups is 07-19. A brand-new sending domain has no reputation, so its mail lands in spam — which predicts exactly this pattern (people sign up, never see the confirmation, never confirm) with no bots required. A 76% non-confirmation rate is unremarkable for a new sender.

    Some remain genuinely odd (the SMS-gateway address especially), so it is likely a mix — and there is no way to sort them. The asymmetry decides it: keeping 13 inert rows costs nothing, while deleting one genuinely interested person is silent and unrecoverable for them. Keep them.

    Sign-up confirmation email deliverability is unverified — DMARC has no reporting, and dead Mailgun SPF/MX remain after the Resend cutover #356 tracks the real question — are confirmation emails actually being delivered? If they are not, that is a bug hurting real users and it outranks this.

  • Add an E2E/smoke assertion that sign-up rejects a request with no/invalid CAPTCHA token, so the protection can't regress unnoticed — the @smoke suite from feat(#288): post-deploy @smoke suite + fix(#348) the CSS-as-script bug it caught #349 already hits live prod and is the natural home.

Why this belongs to the #280 arc

#288 closed on "the suite is green while the deployed product is unusable." This is the mirror image: sign-up now works, and the first thing that found it was a bot. An enterprise-ready template needs the sign-up path to be both functional and abuse-resistant — shipping a template whose default signup is an open mail relay would propagate the flaw to every fork.

Related: #290 (Resend SMTP live), #288 (real-signup integrity), #350 (auth-config drift gate), #349 (@smoke suite).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenterpriseEnterprise-readiness umbrella + gating work (the eval-3 → eval-1 arc)priority:p1High — fix soon (stability hotspot, low-hanging fruit, single-decision unlocks)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions