Skip to content

Commit b2c931d

Browse files
committed
refactor(auth): make MX signup validation opt-in (SIGNUP_MX_VALIDATION_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.
1 parent 078c469 commit b2c931d

5 files changed

Lines changed: 13 additions & 16 deletions

File tree

apps/sim/lib/auth/auth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import {
7878
isOrganizationsEnabled,
7979
isRegistrationDisabled,
8080
isSignupEmailValidationEnabled,
81+
isSignupMxValidationEnabled,
8182
} from '@/lib/core/config/feature-flags'
8283
import { PlatformEvents } from '@/lib/core/telemetry'
8384
import { getBaseUrl, isLocalhostUrl, parseOriginList } from '@/lib/core/utils/urls'
@@ -844,7 +845,7 @@ export const auth = betterAuth({
844845
})
845846
}
846847

847-
if (ctx.path.startsWith('/sign-up/email') && ctx.body?.email) {
848+
if (isSignupMxValidationEnabled && ctx.path.startsWith('/sign-up/email') && ctx.body?.email) {
848849
const mxCheck = await validateSignupEmailMx(ctx.body.email)
849850
if (!mxCheck.allowed) {
850851
throw new APIError('FORBIDDEN', {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export const env = createEnv({
2727
ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login
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")
30-
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.
31-
DISABLE_SIGNUP_MX_VALIDATION: z.boolean().optional(), // Kill switch to disable MX-based signup validation without a deploy
30+
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.
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/core/config/feature-flags.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED
8181
*/
8282
export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED)
8383

84+
/**
85+
* Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam
86+
* mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on
87+
* self-hosted deployments with non-standard mail setups; enable on abuse-targeted deployments.
88+
*/
89+
export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED)
90+
8491
/**
8592
* Is Trigger.dev enabled for async job processing
8693
*/

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const { mockResolveMx, envRef } = vi.hoisted(() => ({
77
mockResolveMx: vi.fn(),
88
envRef: {
99
BLOCKED_EMAIL_MX_HOSTS: undefined as string | undefined,
10-
DISABLE_SIGNUP_MX_VALIDATION: false,
1110
},
1211
}))
1312

@@ -30,7 +29,6 @@ describe('validateSignupEmailMx', () => {
3029
beforeEach(() => {
3130
vi.clearAllMocks()
3231
envRef.BLOCKED_EMAIL_MX_HOSTS = undefined
33-
envRef.DISABLE_SIGNUP_MX_VALIDATION = false
3432
})
3533

3634
it('blocks the known shared spam backend 215.im', async () => {
@@ -83,14 +81,6 @@ describe('validateSignupEmailMx', () => {
8381
expect(result.reason).toBe('blocked_mx_backend')
8482
})
8583

86-
it('respects the DISABLE_SIGNUP_MX_VALIDATION kill switch', async () => {
87-
envRef.DISABLE_SIGNUP_MX_VALIDATION = true
88-
mockResolveMx.mockResolvedValue(mx('smtp.215.im'))
89-
const result = await validateSignupEmailMx('simuser_abc@lyi25swr.cn')
90-
expect(result.allowed).toBe(true)
91-
expect(mockResolveMx).not.toHaveBeenCalled()
92-
})
93-
9484
it('allows when the email has no domain (defers to other validation)', async () => {
9585
const result = await validateSignupEmailMx('not-an-email')
9686
expect(result.allowed).toBe(true)

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,10 @@ export interface SignupEmailCheck {
4141
* users are never blocked by an infrastructure blip. Only a definitive
4242
* "domain has no MX" answer (`ENOTFOUND` / `ENODATA`) blocks.
4343
*
44-
* Server-only — imports `dns/promises`. Never import from client code.
44+
* Server-only — imports `dns/promises`. Never import from client code. Gated by the caller
45+
* behind `isSignupMxValidationEnabled`; this function performs the check unconditionally.
4546
*/
4647
export async function validateSignupEmailMx(email: string): Promise<SignupEmailCheck> {
47-
if (env.DISABLE_SIGNUP_MX_VALIDATION) return { allowed: true }
48-
4948
const domain = email.split('@')[1]?.toLowerCase()
5049
if (!domain) return { allowed: true }
5150

0 commit comments

Comments
 (0)