Skip to content

fix(auth): block signup spam by denylisting shared MX backends#4790

Merged
waleedlatif1 merged 4 commits into
stagingfrom
waleedlatif1/signup-mx-validation
May 29, 2026
Merged

fix(auth): block signup spam by denylisting shared MX backends#4790
waleedlatif1 merged 4 commits into
stagingfrom
waleedlatif1/signup-mx-validation

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

@waleedlatif1 waleedlatif1 commented May 29, 2026

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.

  • New server-only validateSignupEmailMx() resolves MX during POST /sign-up/email and rejects no-MX domains (no_mx) and denylisted backends (blocked_mx_backend).
  • Opt-in via SIGNUP_MX_VALIDATION_ENABLED (default off) — consistent with the sibling SIGNUP_EMAIL_VALIDATION_ENABLED flag. The flag doubles as the kill switch.
  • No backends are hardcoded. The denylist is entirely operator-supplied via 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.
  • Fail-open: any DNS timeout / transient resolver error allows the signup, so legit users are never blocked by an infra blip. Only a definitive "no MX" answer blocks.
  • Returns a clean 403 (APIError), not a 500. The MX-lookup timeout is cleared on resolve to avoid dangling timers.

Why opt-in (not opt-out)

  • Codebase consistency: the directly-analogous disposable-email feature (SIGNUP_EMAIL_VALIDATION_ENABLED) is opt-in.
  • Self-hosted blast radius: MX / no-MX blocking is not near-zero-false-positive across heterogeneous self-hosted mail setups (internal domains, custom MX). Default-on would change signup behavior for every self-hosted instance on a routine upgrade.
  • The attack is on hosted: opt-in fully protects production by setting the env vars where the attack actually is, with zero downside for self-hosters.

Rollout

On the affected deployment, set SIGNUP_MX_VALIDATION_ENABLED=true and populate BLOCKED_EMAIL_MX_HOSTS with the observed shared backends (kept in secrets, not in the repo).

Testing

  • 8 unit tests: env-driven denylist blocks, case-insensitive match, empty denylist blocks nothing (no hardcoded defaults), legit domains allowed, no-MX blocked, empty-MX blocked, fail-open on transient DNS error, missing-domain passthrough
  • bun run type-check clean, bun run lint clean, bun run test app/api/auth/ 60/60 passing

Note 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)

  • Provision a paid email-reputation API (IPQS/ZeroBounce) for fresh-domain/catch-all intel — free disposable lists do not catch fresh domains (verified)
  • Optionally block obvious local-part bot signatures
  • The static BLOCKED_SIGNUP_DOMAINS list can largely be retired in favor of the MX denylist

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

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.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 29, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 29, 2026 6:32pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 29, 2026

PR Summary

Medium Risk
Changes the signup auth path and adds a DNS dependency, but the feature is off by default, fail-open on resolver errors, and only affects /sign-up/email when explicitly enabled.

Overview
Adds opt-in MX-based signup checks for email/password sign-up (/sign-up/email), gated by SIGNUP_MX_VALIDATION_ENABLED (default off) and the isSignupMxValidationEnabled feature flag.

A new server-only validateSignupEmailMx() resolves the domain’s MX records (3s timeout), rejects signups when there is no MX or when any exchange matches operator-configured BLOCKED_EMAIL_MX_HOSTS substrings (no hardcoded backends). Transient DNS failures fail open; definitive no-MX answers block. The auth before hook returns the same 403 message as existing domain blocks.

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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 29, 2026

Greptile Summary

This PR adds an opt-in MX-based signup filter (SIGNUP_MX_VALIDATION_ENABLED) that resolves the MX backend of a signup email at request time and blocks domains with no mail exchanger or whose resolved host matches an operator-supplied substring denylist (BLOCKED_EMAIL_MX_HOSTS). All previous review findings (bare-truthiness env check, dangling timer) are fully addressed in the current state of the branch.

  • validation.server.ts: New server-only async validator using dns/promises, with a 3-second Promise.race timeout, definitive-block on ENOTFOUND/ENODATA, fail-open on every other error, and case-insensitive substring match against the denylist. The finally block correctly clears the timeout handle.
  • auth.ts: MX check is wired into the onRequest hook, gated by isSignupMxValidationEnabled, scoped to startsWith('/sign-up/email'), and consistent in style with the adjacent domain-block check.
  • env.ts / feature-flags.ts: Two new env vars follow existing conventions; isTruthy() is used for the boolean flag, matching every other opt-in flag in the file.

Confidence Score: 5/5

Safe 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

Filename Overview
apps/sim/lib/messaging/email/validation.server.ts New server-only MX validator: resolves MX with a 3-second timeout, blocks ENOTFOUND/ENODATA, fails open on transient errors, and substring-matches against operator-supplied denylist. Logic is sound; dangling-timer issue is addressed with the finally block.
apps/sim/lib/messaging/email/validation.server.test.ts 8 unit tests covering denylist blocking, case-insensitive matching, empty denylist, legit domain, no-MX (ENOTFOUND), empty-MX array, fail-open (ETIMEOUT), and missing domain passthrough. Good branch coverage.
apps/sim/lib/auth/auth.ts Adds MX validation gated behind isSignupMxValidationEnabled on /sign-up/email path; consistent with existing email-domain block pattern directly above it. Throws 403 APIError on rejection.
apps/sim/lib/core/config/feature-flags.ts Adds isSignupMxValidationEnabled using isTruthy(), consistent with all other boolean feature flags in the file.
apps/sim/lib/core/config/env.ts Two new optional env vars added (SIGNUP_MX_VALIDATION_ENABLED as z.boolean(), BLOCKED_EMAIL_MX_HOSTS as z.string()); follow established formatting conventions and are correctly optional.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (3): Last reviewed commit: "refactor(auth): remove hardcoded MX deny..." | Re-trigger Greptile

Comment thread apps/sim/lib/messaging/email/validation.server.ts Outdated
Comment thread apps/sim/lib/messaging/email/validation.server.ts
Comment thread apps/sim/lib/messaging/email/validation.server.ts
…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.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1 waleedlatif1 merged commit dc6073e into staging May 29, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/signup-mx-validation branch May 29, 2026 18:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant