From 6193c4a9483087d22b4716cfa5916e39a53a3380 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 30 May 2026 20:56:26 -0700 Subject: [PATCH] fix(sso): re-check domain conflict before write and reject IP-address domains --- apps/sim/app/api/auth/sso/register/route.ts | 44 ++++++++++++++------- apps/sim/lib/auth/sso/domain.test.ts | 6 +++ apps/sim/lib/auth/sso/domain.ts | 5 ++- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/apps/sim/app/api/auth/sso/register/route.ts b/apps/sim/app/api/auth/sso/register/route.ts index 235116fc9e..d564960704 100644 --- a/apps/sim/app/api/auth/sso/register/route.ts +++ b/apps/sim/app/api/auth/sso/register/route.ts @@ -81,28 +81,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => { return orgId ? provider.organizationId === orgId : false } - const existingProviders = await db - .select({ - userId: ssoProvider.userId, - organizationId: ssoProvider.organizationId, - }) - .from(ssoProvider) - .where(sql`lower(${ssoProvider.domain}) = ${domain}`) - const conflictingProvider = existingProviders.find((provider) => !isOwnedByCaller(provider)) + const findDomainConflict = async () => + ( + await db + .select({ + userId: ssoProvider.userId, + organizationId: ssoProvider.organizationId, + }) + .from(ssoProvider) + .where(sql`lower(${ssoProvider.domain}) = ${domain}`) + ).find((provider) => !isOwnedByCaller(provider)) - if (conflictingProvider) { - logger.warn('Rejected SSO registration for domain owned by another tenant', { - domain, - orgId, - userId: session.user.id, - }) - return NextResponse.json( + const domainConflictResponse = () => + NextResponse.json( { error: 'This domain is already registered for SSO by another organization.', code: 'SSO_DOMAIN_ALREADY_REGISTERED', }, { status: 409 } ) + + if (await findDomainConflict()) { + logger.warn('Rejected SSO registration for domain owned by another tenant', { + domain, + orgId, + userId: session.user.id, + }) + return domainConflictResponse() } const headers: Record = {} @@ -446,6 +451,15 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ), }) + if (await findDomainConflict()) { + logger.warn('Rejected SSO registration: domain was claimed during registration', { + domain, + orgId, + userId: session.user.id, + }) + return domainConflictResponse() + } + const registration = await auth.api.registerSSOProvider({ body: providerConfig, headers, diff --git a/apps/sim/lib/auth/sso/domain.test.ts b/apps/sim/lib/auth/sso/domain.test.ts index 5ca62331a8..333a6576b5 100644 --- a/apps/sim/lib/auth/sso/domain.test.ts +++ b/apps/sim/lib/auth/sso/domain.test.ts @@ -35,4 +35,10 @@ describe('normalizeSSODomain', () => { expect(normalizeSSODomain('not a domain')).toBeNull() expect(normalizeSSODomain('company')).toBeNull() }) + + it('rejects bare IP addresses and numeric TLDs', () => { + expect(normalizeSSODomain('10.0.0.1')).toBeNull() + expect(normalizeSSODomain('192.168.1.1')).toBeNull() + expect(normalizeSSODomain('company.123')).toBeNull() + }) }) diff --git a/apps/sim/lib/auth/sso/domain.ts b/apps/sim/lib/auth/sso/domain.ts index bdd0cc1714..30f6470b15 100644 --- a/apps/sim/lib/auth/sso/domain.ts +++ b/apps/sim/lib/auth/sso/domain.ts @@ -19,7 +19,10 @@ export function normalizeSSODomain(input: string): string | null { value = value.replace(/\.$/, '') if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(value)) return null - if (value.split('.').some((label) => label.length === 0 || label.length > 63)) return null + + const labels = value.split('.') + if (labels.some((label) => label.length === 0 || label.length > 63)) return null + if (/^\d+$/.test(labels[labels.length - 1])) return null return value }