From 947349f2c8a69566fb338577231e3f7fd03f21d0 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:34:59 -0700 Subject: [PATCH 1/9] fix(enterprise): remove dead variables resourceLabel, CHECK_PATH, allFeatures, RESOURCE_TYPE_LABEL --- .../components/enterprise/enterprise.tsx | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/apps/sim/app/(home)/components/enterprise/enterprise.tsx b/apps/sim/app/(home)/components/enterprise/enterprise.tsx index d4646a3074f..17c36b9b17c 100644 --- a/apps/sim/app/(home)/components/enterprise/enterprise.tsx +++ b/apps/sim/app/(home)/components/enterprise/enterprise.tsx @@ -35,25 +35,6 @@ const ACTOR_COLORS: Record = { /** Left accent bar opacity by recency — newest is brightest. */ const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const -/** Human-readable label per resource type. */ -const RESOURCE_TYPE_LABEL: Record = { - workflow: 'Workflow', - member: 'Member', - byok_key: 'BYOK Key', - api_key: 'API Key', - permission_group: 'Permission Group', - credential_set: 'Credential Set', - knowledge_base: 'Knowledge Base', - environment: 'Environment', - mcp_server: 'MCP Server', - file: 'File', - webhook: 'Webhook', - chat: 'Chat', - table: 'Table', - folder: 'Folder', - document: 'Document', -} - interface LogEntry { id: number actor: string @@ -189,7 +170,6 @@ function AuditRow({ entry, index }: AuditRowProps) { const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6' const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04 const timeAgo = formatTimeAgo(entry.insertedAt) - const resourceLabel = RESOURCE_TYPE_LABEL[entry.resourceType] return (
@@ -292,9 +272,6 @@ function AuditLogPreview() { ) } -const CHECK_PATH = - 'M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425z' - interface PermissionFeature { name: string key: string @@ -377,8 +354,6 @@ function AccessControlPanel() { const isInView = useInView(ref, { once: true, margin: '-40px' }) const [accessState, setAccessState] = useState>(INITIAL_ACCESS_STATE) - const allFeatures = PERMISSION_CATEGORIES.flatMap((c) => c.features) - return (
From 67b6a5ee2058de43cfb77188faa6964f4670625e Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:37:24 -0700 Subject: [PATCH 2/9] fix: cap MX cache size, deduplicate validateCallbackUrl, add slug duplicate guard --- apps/sim/app/(auth)/login/login-form.tsx | 6 +++--- apps/sim/app/(landing)/integrations/[slug]/page.tsx | 8 ++++++++ apps/sim/lib/messaging/email/validation.ts | 12 ++++++++++-- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index f6e842a602f..5cbc190273d 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -106,13 +106,13 @@ export default function LoginPage({ const buttonClass = useBrandedButtonClass() const callbackUrlParam = searchParams?.get('callbackUrl') + const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false const invalidCallbackRef = useRef(false) - if (callbackUrlParam && !validateCallbackUrl(callbackUrlParam) && !invalidCallbackRef.current) { + if (callbackUrlParam && !isValidCallbackUrl && !invalidCallbackRef.current) { invalidCallbackRef.current = true logger.warn('Invalid callback URL detected and blocked:', { url: callbackUrlParam }) } - const callbackUrl = - callbackUrlParam && validateCallbackUrl(callbackUrlParam) ? callbackUrlParam : '/workspace' + const callbackUrl = isValidCallbackUrl ? callbackUrlParam! : '/workspace' const isInviteFlow = searchParams?.get('invite_flow') === 'true' const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false) diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index 693e0fee68d..02ddd5d9073 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -18,6 +18,14 @@ const byName = new Map(allIntegrations.map((i) => [i.name, i])) const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) +if (process.env.NODE_ENV === 'development') { + const slugsSeen = new Set() + for (const i of allIntegrations) { + if (slugsSeen.has(i.slug)) throw new Error(`Duplicate integration slug: ${i.slug}`) + slugsSeen.add(i.slug) + } +} + /** Returns workflow pairs that feature the given integration on either side. */ function getPairsFor(name: string) { return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name) diff --git a/apps/sim/lib/messaging/email/validation.ts b/apps/sim/lib/messaging/email/validation.ts index 821dc5a9f1b..372f9d2a479 100644 --- a/apps/sim/lib/messaging/email/validation.ts +++ b/apps/sim/lib/messaging/email/validation.ts @@ -64,6 +64,14 @@ const DISPOSABLE_MX_BACKENDS = new Set(['in.mail.gw', 'smtp.catchmail.io', 'mx.y /** Per-domain MX result cache — avoids redundant DNS queries for concurrent or repeated sign-ups */ const mxCache = new Map() +const MX_CACHE_MAX = 1_000 + +function setMxCache(domain: string, entry: { result: boolean; expires: number }) { + if (mxCache.size >= MX_CACHE_MAX) { + mxCache.delete(mxCache.keys().next().value!) + } + mxCache.set(domain, entry) +} /** * Validates email syntax using RFC 5322 compliant regex @@ -135,10 +143,10 @@ export async function isDisposableMxBackend(email: string): Promise { } ) const result = await Promise.race([mxCheckPromise, timeoutPromise]) - mxCache.set(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 }) + setMxCache(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 }) return result.isDisposableBackend } catch { - mxCache.set(domain, { result: false, expires: now + 60 * 1000 }) + setMxCache(domain, { result: false, expires: now + 60 * 1000 }) return false } finally { clearTimeout(timeoutId) From 6a05854c0277f61d08bb9cb0d52d96ad9f0658ae Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:39:14 -0700 Subject: [PATCH 3/9] revert: remove slug duplicate guard --- apps/sim/app/(landing)/integrations/[slug]/page.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index 02ddd5d9073..693e0fee68d 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -18,14 +18,6 @@ const byName = new Map(allIntegrations.map((i) => [i.name, i])) const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) -if (process.env.NODE_ENV === 'development') { - const slugsSeen = new Set() - for (const i of allIntegrations) { - if (slugsSeen.has(i.slug)) throw new Error(`Duplicate integration slug: ${i.slug}`) - slugsSeen.add(i.slug) - } -} - /** Returns workflow pairs that feature the given integration on either side. */ function getPairsFor(name: string) { return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name) From 730c9ae34ef13d0ca196a05c9a483f7e53975139 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:41:48 -0700 Subject: [PATCH 4/9] refactor: extract validateCallbackUrl to shared util, evict stale MX cache entries on lookup --- apps/sim/app/(auth)/login/login-form.tsx | 19 +------------------ apps/sim/ee/sso/components/sso-form.tsx | 19 +------------------ apps/sim/lib/auth/validate-callback-url.ts | 21 +++++++++++++++++++++ apps/sim/lib/messaging/email/validation.ts | 5 ++++- 4 files changed, 27 insertions(+), 37 deletions(-) create mode 100644 apps/sim/lib/auth/validate-callback-url.ts diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 5cbc190273d..6fce6170f8f 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -15,6 +15,7 @@ import { ModalHeader, } from '@/components/emcn' import { client } from '@/lib/auth/auth-client' +import { validateCallbackUrl } from '@/lib/auth/validate-callback-url' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -53,24 +54,6 @@ const PASSWORD_VALIDATIONS = { }, } -const validateCallbackUrl = (url: string): boolean => { - try { - if (url.startsWith('/')) { - return true - } - - const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' - if (url.startsWith(currentOrigin)) { - return true - } - - return false - } catch (error) { - logger.error('Error validating callback URL:', { error, url }) - return false - } -} - const validatePassword = (passwordValue: string): string[] => { const errors: string[] = [] diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index 22f8aff6c79..6e8883692ab 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -6,6 +6,7 @@ import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { Button, Input, Label } from '@/components/emcn' import { client } from '@/lib/auth/auth-client' +import { validateCallbackUrl } from '@/lib/auth/validate-callback-url' import { env, isFalsy } from '@/lib/core/config/env' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' @@ -29,24 +30,6 @@ const validateEmailField = (emailValue: string): string[] => { return errors } -const validateCallbackUrl = (url: string): boolean => { - try { - if (url.startsWith('/')) { - return true - } - - const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' - if (url.startsWith(currentOrigin)) { - return true - } - - return false - } catch (error) { - logger.error('Error validating callback URL:', { error, url }) - return false - } -} - export default function SSOForm() { const router = useRouter() const searchParams = useSearchParams() diff --git a/apps/sim/lib/auth/validate-callback-url.ts b/apps/sim/lib/auth/validate-callback-url.ts new file mode 100644 index 00000000000..177179b29cd --- /dev/null +++ b/apps/sim/lib/auth/validate-callback-url.ts @@ -0,0 +1,21 @@ +import { createLogger } from '@sim/logger' + +const logger = createLogger('ValidateCallbackUrl') + +/** + * Returns true if the URL is safe to redirect to after authentication. + * Accepts relative paths and absolute URLs matching the current origin. + */ +export function validateCallbackUrl(url: string): boolean { + try { + if (url.startsWith('/')) return true + + const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' + if (url.startsWith(currentOrigin)) return true + + return false + } catch (error) { + logger.error('Error validating callback URL:', { error, url }) + return false + } +} diff --git a/apps/sim/lib/messaging/email/validation.ts b/apps/sim/lib/messaging/email/validation.ts index 372f9d2a479..6b2972a6560 100644 --- a/apps/sim/lib/messaging/email/validation.ts +++ b/apps/sim/lib/messaging/email/validation.ts @@ -132,7 +132,10 @@ export async function isDisposableMxBackend(email: string): Promise { const now = Date.now() const cached = mxCache.get(domain) - if (cached && cached.expires > now) return cached.result + if (cached) { + if (cached.expires > now) return cached.result + mxCache.delete(domain) + } let timeoutId: ReturnType | undefined try { From 96138ca778b15237b398fe4d80982a029a9a2802 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:51:28 -0700 Subject: [PATCH 5/9] refactor: move validateCallbackUrl into input-validation.ts --- apps/sim/app/(auth)/login/login-form.tsx | 2 +- apps/sim/ee/sso/components/sso-form.tsx | 2 +- apps/sim/lib/auth/validate-callback-url.ts | 21 ------------------- .../sim/lib/core/security/input-validation.ts | 21 +++++++++++++++++++ 4 files changed, 23 insertions(+), 23 deletions(-) delete mode 100644 apps/sim/lib/auth/validate-callback-url.ts diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 6fce6170f8f..337a81f9b54 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -15,8 +15,8 @@ import { ModalHeader, } from '@/components/emcn' import { client } from '@/lib/auth/auth-client' -import { validateCallbackUrl } from '@/lib/auth/validate-callback-url' import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env' +import { validateCallbackUrl } from '@/lib/core/security/input-validation' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' import { quickValidateEmail } from '@/lib/messaging/email/validation' diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index 6e8883692ab..bdd46c1afa9 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -6,8 +6,8 @@ import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { Button, Input, Label } from '@/components/emcn' import { client } from '@/lib/auth/auth-client' -import { validateCallbackUrl } from '@/lib/auth/validate-callback-url' import { env, isFalsy } from '@/lib/core/config/env' +import { validateCallbackUrl } from '@/lib/core/security/input-validation' import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' import { BrandedButton } from '@/app/(auth)/components/branded-button' diff --git a/apps/sim/lib/auth/validate-callback-url.ts b/apps/sim/lib/auth/validate-callback-url.ts deleted file mode 100644 index 177179b29cd..00000000000 --- a/apps/sim/lib/auth/validate-callback-url.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createLogger } from '@sim/logger' - -const logger = createLogger('ValidateCallbackUrl') - -/** - * Returns true if the URL is safe to redirect to after authentication. - * Accepts relative paths and absolute URLs matching the current origin. - */ -export function validateCallbackUrl(url: string): boolean { - try { - if (url.startsWith('/')) return true - - const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' - if (url.startsWith(currentOrigin)) return true - - return false - } catch (error) { - logger.error('Error validating callback URL:', { error, url }) - return false - } -} diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index e4dc671196d..eb4bee3cde7 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1169,3 +1169,24 @@ export function validatePaginationCursor( return { isValid: true, sanitized: value } } + +/** + * Validates a callback URL to prevent open redirect attacks. + * Accepts relative paths and absolute URLs matching the current origin. + * + * @param url - The callback URL to validate + * @returns true if the URL is safe to redirect to + */ +export function validateCallbackUrl(url: string): boolean { + try { + if (url.startsWith('/')) return true + + const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' + if (url.startsWith(currentOrigin)) return true + + return false + } catch (error) { + logger.error('Error validating callback URL:', { error, url }) + return false + } +} From df55f5fb5a3239546f33bbdc9a487d8505d85592 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:53:34 -0700 Subject: [PATCH 6/9] fix: guard validateCallbackUrl against server-side window, skip eviction on cache update --- apps/sim/lib/core/security/input-validation.ts | 4 +++- apps/sim/lib/messaging/email/validation.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index eb4bee3cde7..21abb77c545 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1181,7 +1181,9 @@ export function validateCallbackUrl(url: string): boolean { try { if (url.startsWith('/')) return true - const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '' + if (typeof window === 'undefined') return false + + const currentOrigin = window.location.origin if (url.startsWith(currentOrigin)) return true return false diff --git a/apps/sim/lib/messaging/email/validation.ts b/apps/sim/lib/messaging/email/validation.ts index 6b2972a6560..4e076a30e19 100644 --- a/apps/sim/lib/messaging/email/validation.ts +++ b/apps/sim/lib/messaging/email/validation.ts @@ -67,7 +67,7 @@ const mxCache = new Map() const MX_CACHE_MAX = 1_000 function setMxCache(domain: string, entry: { result: boolean; expires: number }) { - if (mxCache.size >= MX_CACHE_MAX) { + if (mxCache.size >= MX_CACHE_MAX && !mxCache.has(domain)) { mxCache.delete(mxCache.keys().next().value!) } mxCache.set(domain, entry) From 551664219b8e6f13e149093107b445fedc91f1ec Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:55:46 -0700 Subject: [PATCH 7/9] fix(auth): remove redundant validateCallbackUrl re-check on already-safe callbackUrl --- apps/sim/app/(auth)/login/login-form.tsx | 2 +- apps/sim/ee/sso/components/sso-form.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 337a81f9b54..9dd5cc78dbc 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -175,7 +175,7 @@ export default function LoginPage({ } try { - const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace' + const safeCallbackUrl = callbackUrl let errorHandled = false const result = await client.signIn.email( diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index bdd46c1afa9..8e8ee3e0be3 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -98,7 +98,7 @@ export default function SSOForm() { } try { - const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace' + const safeCallbackUrl = callbackUrl await client.signIn.sso({ email: emailValue, From 8412f420142095cf0a559f300f6552920d41776e Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:56:16 -0700 Subject: [PATCH 8/9] chore(auth): add comment explaining why safeCallbackUrl skip re-validation --- apps/sim/app/(auth)/login/login-form.tsx | 1 + apps/sim/ee/sso/components/sso-form.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index 9dd5cc78dbc..e4c99253376 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -175,6 +175,7 @@ export default function LoginPage({ } try { + // callbackUrl is already validated at initialization — either a passing callbackUrlParam or '/workspace' const safeCallbackUrl = callbackUrl let errorHandled = false diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index 8e8ee3e0be3..3fcf71902da 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -98,6 +98,7 @@ export default function SSOForm() { } try { + // callbackUrl is already validated at initialization — either a passing callbackUrlParam or '/workspace' const safeCallbackUrl = callbackUrl await client.signIn.sso({ From 1d7652c3eb66c69036b7990f59817edf17070ba5 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 13:58:54 -0700 Subject: [PATCH 9/9] chore: remove redundant inline comments --- apps/sim/app/(auth)/login/login-form.tsx | 1 - apps/sim/ee/sso/components/sso-form.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx index e4c99253376..9dd5cc78dbc 100644 --- a/apps/sim/app/(auth)/login/login-form.tsx +++ b/apps/sim/app/(auth)/login/login-form.tsx @@ -175,7 +175,6 @@ export default function LoginPage({ } try { - // callbackUrl is already validated at initialization — either a passing callbackUrlParam or '/workspace' const safeCallbackUrl = callbackUrl let errorHandled = false diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx index 3fcf71902da..8e8ee3e0be3 100644 --- a/apps/sim/ee/sso/components/sso-form.tsx +++ b/apps/sim/ee/sso/components/sso-form.tsx @@ -98,7 +98,6 @@ export default function SSOForm() { } try { - // callbackUrl is already validated at initialization — either a passing callbackUrlParam or '/workspace' const safeCallbackUrl = callbackUrl await client.signIn.sso({