Skip to content

Commit e62c3ad

Browse files
authored
improvement(auth): suffix-match BLOCKED_SIGNUP_DOMAINS to catch subdomain rotation (#4773)
* improvement(auth): suffix-match BLOCKED_SIGNUP_DOMAINS to catch subdomain rotation * improvement(auth): dedupe denylist entries, extract isEmailInDenylist with tests
1 parent e78ac0f commit e62c3ad

2 files changed

Lines changed: 72 additions & 14 deletions

File tree

apps/sim/lib/auth/auth.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { isEmailInDenylist } from '@/lib/auth/auth'
6+
7+
describe('isEmailInDenylist', () => {
8+
it('returns false when denylist is null, empty, or email is missing', () => {
9+
expect(isEmailInDenylist('a@example.com', null)).toBe(false)
10+
expect(isEmailInDenylist('a@example.com', [])).toBe(false)
11+
expect(isEmailInDenylist(null, ['example.com'])).toBe(false)
12+
expect(isEmailInDenylist(undefined, ['example.com'])).toBe(false)
13+
expect(isEmailInDenylist('', ['example.com'])).toBe(false)
14+
})
15+
16+
it('returns false when email has no @', () => {
17+
expect(isEmailInDenylist('not-an-email', ['example.com'])).toBe(false)
18+
})
19+
20+
it('matches exact domain', () => {
21+
expect(isEmailInDenylist('user@dpdns.org', ['dpdns.org'])).toBe(true)
22+
expect(isEmailInDenylist('user@DPDNS.ORG', ['dpdns.org'])).toBe(true)
23+
})
24+
25+
it('matches arbitrary-depth subdomains of a listed parent zone', () => {
26+
expect(isEmailInDenylist('user@xx.lucky04.dpdns.org', ['dpdns.org'])).toBe(true)
27+
expect(isEmailInDenylist('user@a.b.c.qzz.io', ['qzz.io'])).toBe(true)
28+
})
29+
30+
it('does not match look-alike domains', () => {
31+
expect(isEmailInDenylist('user@xdpdns.org', ['dpdns.org'])).toBe(false)
32+
expect(isEmailInDenylist('user@notdpdns.org', ['dpdns.org'])).toBe(false)
33+
})
34+
35+
it('does not match disallowed domains', () => {
36+
expect(isEmailInDenylist('user@gmail.com', ['dpdns.org', 'qzz.io'])).toBe(false)
37+
expect(isEmailInDenylist('user@example.com', ['dpdns.org'])).toBe(false)
38+
})
39+
40+
it('handles multiple denylist entries', () => {
41+
const denylist = ['dpdns.org', 'qzz.io', 'cc.cd']
42+
expect(isEmailInDenylist('user@foo.dpdns.org', denylist)).toBe(true)
43+
expect(isEmailInDenylist('user@bar.qzz.io', denylist)).toBe(true)
44+
expect(isEmailInDenylist('user@baz.cc.cd', denylist)).toBe(true)
45+
expect(isEmailInDenylist('user@example.com', denylist)).toBe(false)
46+
})
47+
})

apps/sim/lib/auth/auth.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,29 @@ function getMicrosoftUserInfoFromIdToken(tokens: { accessToken?: string }, provi
143143
}
144144

145145
const blockedSignupDomains = env.BLOCKED_SIGNUP_DOMAINS
146-
? new Set(env.BLOCKED_SIGNUP_DOMAINS.split(',').map((d) => d.trim().toLowerCase()))
146+
? Array.from(
147+
new Set(
148+
env.BLOCKED_SIGNUP_DOMAINS.split(',')
149+
.map((d) => d.trim().toLowerCase())
150+
.filter(Boolean)
151+
)
152+
)
147153
: null
148154

155+
export function isEmailInDenylist(
156+
email: string | undefined | null,
157+
denylist: readonly string[] | null
158+
): boolean {
159+
if (!denylist || denylist.length === 0 || !email) return false
160+
const domain = email.split('@')[1]?.toLowerCase()
161+
if (!domain) return false
162+
return denylist.some((entry) => domain === entry || domain.endsWith(`.${entry}`))
163+
}
164+
165+
function isSignupEmailBlocked(email: string | undefined | null): boolean {
166+
return isEmailInDenylist(email, blockedSignupDomains)
167+
}
168+
149169
const additionalTrustedOrigins = parseOriginList(env.TRUSTED_ORIGINS, (value) =>
150170
logger.warn('Ignoring invalid entry in TRUSTED_ORIGINS', { value })
151171
)
@@ -219,11 +239,8 @@ export const auth = betterAuth({
219239
user: {
220240
create: {
221241
before: async (user) => {
222-
if (blockedSignupDomains) {
223-
const emailDomain = user.email?.split('@')[1]?.toLowerCase()
224-
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
225-
throw new Error('Sign-ups from this email domain are not allowed.')
226-
}
242+
if (isSignupEmailBlocked(user.email)) {
243+
throw new Error('Sign-ups from this email domain are not allowed.')
227244
}
228245
return { data: user }
229246
},
@@ -814,14 +831,8 @@ export const auth = betterAuth({
814831
}
815832
}
816833

817-
if (ctx.path.startsWith('/sign-up') && blockedSignupDomains) {
818-
const requestEmail = ctx.body?.email?.toLowerCase()
819-
if (requestEmail) {
820-
const emailDomain = requestEmail.split('@')[1]
821-
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
822-
throw new Error('Sign-ups from this email domain are not allowed.')
823-
}
824-
}
834+
if (ctx.path.startsWith('/sign-up') && isSignupEmailBlocked(ctx.body?.email)) {
835+
throw new Error('Sign-ups from this email domain are not allowed.')
825836
}
826837

827838
if (ctx.path === '/oauth2/authorize' || ctx.path === '/oauth2/token') {

0 commit comments

Comments
 (0)