fix(auth): block signup spam by denylisting shared MX backends#4790
Conversation
Signup-spam bots rotate throwaway domains rapidly but funnel them through a small number of shared catch-all mail providers. Across the current wave, 85% of bot domains resolved to just two MX backends (smtp.215.im, email.gravityengine.cc), while every domain differed — so the resolved MX host is a far more durable signal than the domain itself. Add a server-only MX validator (validateSignupEmailMx) that resolves the domain's MX records during /sign-up/email and rejects: - domains with no MX record (no_mx) - domains whose MX backend is on the denylist (blocked_mx_backend) Seeded with the two observed backends; extend at runtime via BLOCKED_EMAIL_MX_HOSTS. Fail-open on DNS timeout/transient error so legitimate users are never blocked by a resolver blip; kill switch via DISABLE_SIGNUP_MX_VALIDATION. Returns a clean 403 (APIError), not a 500.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview A new server-only Env schema documents the two new variables; eight unit tests cover denylist matching, no-MX, fail-open, and malformed email passthrough. Reviewed by Cursor Bugbot for commit 457dea6. Configure here. |
Greptile SummaryThis PR adds an opt-in MX-based signup filter (
Confidence Score: 5/5Safe to merge — the change is opt-in by default, scoped to a single hook, and fail-open on DNS errors. The validator is only invoked when the operator explicitly sets SIGNUP_MX_VALIDATION_ENABLED, so no existing deployments are affected by default. The fail-open design means a DNS blip never blocks legitimate signups. The only findings are minor style and test-coverage observations with no impact on correctness or security. No files require special attention. validation.server.ts is the core new logic and is straightforward to reason about. Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant BetterAuth as Better Auth (onRequest hook)
participant Validator as validateSignupEmailMx()
participant DNS as dns.resolveMx()
Client->>BetterAuth: "POST /sign-up/email { email }"
BetterAuth->>BetterAuth: isSignupMxValidationEnabled?
alt flag disabled
BetterAuth-->>Client: continue (no MX check)
else flag enabled
BetterAuth->>Validator: validateSignupEmailMx(email)
Validator->>DNS: resolveMx(domain) [3s timeout]
alt ENOTFOUND / ENODATA
DNS-->>Validator: error (no MX)
Validator-->>BetterAuth: "allowed=false, reason=no_mx"
BetterAuth-->>Client: 403 Forbidden
else transient error / timeout
DNS-->>Validator: error (transient)
Validator-->>BetterAuth: "allowed=true (fail-open)"
BetterAuth-->>Client: continue signup
else MX records returned
DNS-->>Validator: MxRecord[]
alt host matches BLOCKED_EMAIL_MX_HOSTS
Validator-->>BetterAuth: "allowed=false, reason=blocked_mx_backend"
BetterAuth-->>Client: 403 Forbidden
else no match
Validator-->>BetterAuth: "allowed=true"
BetterAuth-->>Client: continue signup
end
end
end
Reviews (3): Last reviewed commit: "refactor(auth): remove hardcoded MX deny..." | Re-trigger Greptile |
…N_ENABLED) Aligns with the sibling feature SIGNUP_EMAIL_VALIDATION_ENABLED (disposable blocking via harmony), which is also opt-in. Default-off avoids adding a DNS dependency to the signup path and prevents surprise signup blocking on self-hosted deployments with non-standard mail setups (internal domains, or a too-broad MX entry catching legit shared infra like Cloudflare Email Routing). Enable on hosted/abuse-targeted deployments via SIGNUP_MX_VALIDATION_ENABLED; the flag doubles as the kill switch, so the separate DISABLE_ flag is removed.
|
@greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 725f380. Configure here.
The MX-backend denylist is now entirely operator-supplied via BLOCKED_EMAIL_MX_HOSTS. Sim is open source, so no specific mail backends are named in the repo, the env example, or the tests — deployments configure their own list out of band (e.g. via secrets). The no-MX hygiene check is unchanged; with an empty denylist no backend is blocked.
|
@greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 457dea6. Configure here.
Summary
Signup-spam bots rotate throwaway domains rapidly but funnel them through a small number of shared catch-all mail backends. In the wave we investigated, the large majority of bot domains resolved to just a couple of MX hosts while every domain differed. The resolved MX backend is a far more durable signal than the domain, so denylisting it hits the chokepoint instead of playing domain whack-a-mole. This is a recognized best-practice layer (DNS/MX infrastructure analysis) — disposable providers rotate domain names but reuse backend mail infrastructure.
validateSignupEmailMx()resolves MX duringPOST /sign-up/emailand rejects no-MX domains (no_mx) and denylisted backends (blocked_mx_backend).SIGNUP_MX_VALIDATION_ENABLED(default off) — consistent with the siblingSIGNUP_EMAIL_VALIDATION_ENABLEDflag. The flag doubles as the kill switch.BLOCKED_EMAIL_MX_HOSTS(comma-separated host substrings), configured out of band (e.g. secrets) — appropriate for an open-source repo. Extendable with no deploy.APIError), not a 500. The MX-lookup timeout is cleared on resolve to avoid dangling timers.Why opt-in (not opt-out)
SIGNUP_EMAIL_VALIDATION_ENABLED) is opt-in.Rollout
On the affected deployment, set
SIGNUP_MX_VALIDATION_ENABLED=trueand populateBLOCKED_EMAIL_MX_HOSTSwith the observed shared backends (kept in secrets, not in the repo).Testing
bun run type-checkclean,bun run lintclean,bun run test app/api/auth/60/60 passingNote for merge
Recommend squash-merge so the final commit message on staging is clean (intermediate branch commits referenced specific backends during investigation; those should not land on the default branch).
Follow-ups (not in this PR)
BLOCKED_SIGNUP_DOMAINSlist can largely be retired in favor of the MX denylistChecklist