Skip to content

Commit 457dea6

Browse files
committed
refactor(auth): remove hardcoded MX denylist defaults
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.
1 parent 725f380 commit 457dea6

3 files changed

Lines changed: 27 additions & 26 deletions

File tree

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const env = createEnv({
2828
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login
2929
BLOCKED_SIGNUP_DOMAINS: z.string().optional(), // Comma-separated list of email domains blocked from signing up (e.g., "gmail.com,yahoo.com")
3030
SIGNUP_MX_VALIDATION_ENABLED: z.boolean().optional(), // Opt-in: validate the email's MX backend at signup (blocks no-MX domains and denylisted shared spam backends). Off by default; enable on hosted/abuse-targeted deployments.
31-
BLOCKED_EMAIL_MX_HOSTS: z.string().optional(), // Comma-separated MX-host substrings blocked from signing up; matches the domain's resolved MX backend (e.g., "215.im,gravityengine.cc"). Catches throwaway domains that share a mail backend. Merged with built-in defaults. Only used when SIGNUP_MX_VALIDATION_ENABLED is set.
31+
BLOCKED_EMAIL_MX_HOSTS: z.string().optional(), // Comma-separated MX-host substrings blocked from signing up; matched against the domain's resolved MX backend to catch throwaway domains that share a mail backend. No defaults — operators supply their own list. Only used when SIGNUP_MX_VALIDATION_ENABLED is set.
3232
TRUSTED_ORIGINS: z.string().optional(), // Comma-separated additional origins to trust for auth (e.g., "https://app.example.com,https://www.example.com"). Merged into Better Auth trustedOrigins.
3333
TURNSTILE_SECRET_KEY: z.string().min(1).optional(), // Cloudflare Turnstile secret key for captcha verification
3434
SIGNUP_EMAIL_VALIDATION_ENABLED: z.boolean().optional(), // Enable disposable email blocking via better-auth-harmony (55K+ domains)

apps/sim/lib/messaging/email/validation.server.test.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,29 @@ describe('validateSignupEmailMx', () => {
3131
envRef.BLOCKED_EMAIL_MX_HOSTS = undefined
3232
})
3333

34-
it('blocks the known shared spam backend 215.im', async () => {
35-
mockResolveMx.mockResolvedValue(mx('smtp.215.im'))
36-
const result = await validateSignupEmailMx('simuser_abc@lyi25swr.cn')
34+
it('blocks a domain whose MX backend is on the configured denylist', async () => {
35+
envRef.BLOCKED_EMAIL_MX_HOSTS = 'blocked-backend.example'
36+
mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example'))
37+
const result = await validateSignupEmailMx('user@rotated-domain.test')
3738
expect(result.allowed).toBe(false)
3839
expect(result.reason).toBe('blocked_mx_backend')
3940
})
4041

41-
it('blocks gravityengine.cc backend', async () => {
42-
mockResolveMx.mockResolvedValue(mx('email.gravityengine.cc'))
43-
const result = await validateSignupEmailMx('x@acgfun.eu.org')
42+
it('matches the denylist as a case-insensitive substring of the MX exchange', async () => {
43+
envRef.BLOCKED_EMAIL_MX_HOSTS = 'Blocked-Backend.Example'
44+
mockResolveMx.mockResolvedValue(mx('mx1.blocked-backend.example'))
45+
const result = await validateSignupEmailMx('user@another-domain.test')
4446
expect(result.allowed).toBe(false)
4547
expect(result.reason).toBe('blocked_mx_backend')
4648
})
4749

50+
it('does not block any backend when the denylist is empty (no hardcoded defaults)', async () => {
51+
envRef.BLOCKED_EMAIL_MX_HOSTS = undefined
52+
mockResolveMx.mockResolvedValue(mx('smtp.blocked-backend.example'))
53+
const result = await validateSignupEmailMx('user@rotated-domain.test')
54+
expect(result.allowed).toBe(true)
55+
})
56+
4857
it('allows a legitimate domain (gmail)', async () => {
4958
mockResolveMx.mockResolvedValue(
5059
mx('gmail-smtp-in.l.google.com', 'alt1.gmail-smtp-in.l.google.com')
@@ -73,14 +82,6 @@ describe('validateSignupEmailMx', () => {
7382
expect(result.allowed).toBe(true)
7483
})
7584

76-
it('honors additional backends from BLOCKED_EMAIL_MX_HOSTS', async () => {
77-
envRef.BLOCKED_EMAIL_MX_HOSTS = 'newbadhost.example'
78-
mockResolveMx.mockResolvedValue(mx('mx1.newbadhost.example'))
79-
const result = await validateSignupEmailMx('x@rotated-domain.top')
80-
expect(result.allowed).toBe(false)
81-
expect(result.reason).toBe('blocked_mx_backend')
82-
})
83-
8485
it('allows when the email has no domain (defers to other validation)', async () => {
8586
const result = await validateSignupEmailMx('not-an-email')
8687
expect(result.allowed).toBe(true)

apps/sim/lib/messaging/email/validation.server.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,23 @@ import { env } from '@/lib/core/config/env'
66

77
const logger = createLogger('EmailValidationServer')
88

9-
/**
10-
* Mail backends abused by signup-spam botnets. The bots rotate throwaway
11-
* domains rapidly but funnel them through a small number of shared catch-all
12-
* mail providers, so the resolved MX host is a far more stable signal than the
13-
* domain itself. Matched as a case-insensitive substring against each MX
14-
* exchange. Extend at runtime via `BLOCKED_EMAIL_MX_HOSTS`.
15-
*/
16-
const DEFAULT_BLOCKED_MX_HOSTS = ['215.im', 'gravityengine.cc'] as const
17-
189
const MX_LOOKUP_TIMEOUT_MS = 3000
1910

11+
/**
12+
* MX-host substrings to block, supplied at runtime via `BLOCKED_EMAIL_MX_HOSTS`.
13+
*
14+
* Signup-spam botnets rotate throwaway domains rapidly but funnel them through a
15+
* small number of shared catch-all mail providers, so the resolved MX host is a
16+
* far more stable signal than the domain itself. Each entry is matched as a
17+
* case-insensitive substring against the domain's resolved MX exchanges. No
18+
* hosts are hardcoded — operators configure their own denylist out of band.
19+
*/
2020
function getBlockedMxHosts(): string[] {
21-
const extra =
21+
return (
2222
env.BLOCKED_EMAIL_MX_HOSTS?.split(',')
2323
.map((h) => h.trim().toLowerCase())
2424
.filter(Boolean) ?? []
25-
return [...DEFAULT_BLOCKED_MX_HOSTS, ...extra]
25+
)
2626
}
2727

2828
export interface SignupEmailCheck {

0 commit comments

Comments
 (0)