Skip to content

Commit 413c45d

Browse files
authored
improvement(platform): landing page cleanup, MX cache fixes, and auth util extraction (#3683)
* fix(enterprise): remove dead variables resourceLabel, CHECK_PATH, allFeatures, RESOURCE_TYPE_LABEL * fix: cap MX cache size, deduplicate validateCallbackUrl, add slug duplicate guard * revert: remove slug duplicate guard * refactor: extract validateCallbackUrl to shared util, evict stale MX cache entries on lookup * refactor: move validateCallbackUrl into input-validation.ts * fix: guard validateCallbackUrl against server-side window, skip eviction on cache update * fix(auth): remove redundant validateCallbackUrl re-check on already-safe callbackUrl * chore(auth): add comment explaining why safeCallbackUrl skip re-validation * chore: remove redundant inline comments
1 parent 30b7192 commit 413c45d

File tree

5 files changed

+44
-69
lines changed

5 files changed

+44
-69
lines changed

apps/sim/app/(auth)/login/login-form.tsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '@/components/emcn'
1717
import { client } from '@/lib/auth/auth-client'
1818
import { getEnv, isFalsy, isTruthy } from '@/lib/core/config/env'
19+
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
1920
import { cn } from '@/lib/core/utils/cn'
2021
import { getBaseUrl } from '@/lib/core/utils/urls'
2122
import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -53,24 +54,6 @@ const PASSWORD_VALIDATIONS = {
5354
},
5455
}
5556

56-
const validateCallbackUrl = (url: string): boolean => {
57-
try {
58-
if (url.startsWith('/')) {
59-
return true
60-
}
61-
62-
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
63-
if (url.startsWith(currentOrigin)) {
64-
return true
65-
}
66-
67-
return false
68-
} catch (error) {
69-
logger.error('Error validating callback URL:', { error, url })
70-
return false
71-
}
72-
}
73-
7457
const validatePassword = (passwordValue: string): string[] => {
7558
const errors: string[] = []
7659

@@ -106,13 +89,13 @@ export default function LoginPage({
10689
const buttonClass = useBrandedButtonClass()
10790

10891
const callbackUrlParam = searchParams?.get('callbackUrl')
92+
const isValidCallbackUrl = callbackUrlParam ? validateCallbackUrl(callbackUrlParam) : false
10993
const invalidCallbackRef = useRef(false)
110-
if (callbackUrlParam && !validateCallbackUrl(callbackUrlParam) && !invalidCallbackRef.current) {
94+
if (callbackUrlParam && !isValidCallbackUrl && !invalidCallbackRef.current) {
11195
invalidCallbackRef.current = true
11296
logger.warn('Invalid callback URL detected and blocked:', { url: callbackUrlParam })
11397
}
114-
const callbackUrl =
115-
callbackUrlParam && validateCallbackUrl(callbackUrlParam) ? callbackUrlParam : '/workspace'
98+
const callbackUrl = isValidCallbackUrl ? callbackUrlParam! : '/workspace'
11699
const isInviteFlow = searchParams?.get('invite_flow') === 'true'
117100

118101
const [forgotPasswordOpen, setForgotPasswordOpen] = useState(false)
@@ -192,7 +175,7 @@ export default function LoginPage({
192175
}
193176

194177
try {
195-
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
178+
const safeCallbackUrl = callbackUrl
196179
let errorHandled = false
197180

198181
const result = await client.signIn.email(

apps/sim/app/(home)/components/enterprise/enterprise.tsx

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,6 @@ const ACTOR_COLORS: Record<string, string> = {
3535
/** Left accent bar opacity by recency — newest is brightest. */
3636
const ACCENT_OPACITIES = [0.75, 0.5, 0.35, 0.22, 0.12, 0.05] as const
3737

38-
/** Human-readable label per resource type. */
39-
const RESOURCE_TYPE_LABEL: Record<string, string> = {
40-
workflow: 'Workflow',
41-
member: 'Member',
42-
byok_key: 'BYOK Key',
43-
api_key: 'API Key',
44-
permission_group: 'Permission Group',
45-
credential_set: 'Credential Set',
46-
knowledge_base: 'Knowledge Base',
47-
environment: 'Environment',
48-
mcp_server: 'MCP Server',
49-
file: 'File',
50-
webhook: 'Webhook',
51-
chat: 'Chat',
52-
table: 'Table',
53-
folder: 'Folder',
54-
document: 'Document',
55-
}
56-
5738
interface LogEntry {
5839
id: number
5940
actor: string
@@ -189,7 +170,6 @@ function AuditRow({ entry, index }: AuditRowProps) {
189170
const color = ACTOR_COLORS[entry.actor] ?? '#F6F6F6'
190171
const accentOpacity = ACCENT_OPACITIES[index] ?? 0.04
191172
const timeAgo = formatTimeAgo(entry.insertedAt)
192-
const resourceLabel = RESOURCE_TYPE_LABEL[entry.resourceType]
193173

194174
return (
195175
<div className='group relative overflow-hidden border-[#2A2A2A] border-b bg-[#1C1C1C] transition-colors duration-150 last:border-b-0 hover:bg-[#212121]'>
@@ -292,9 +272,6 @@ function AuditLogPreview() {
292272
)
293273
}
294274

295-
const CHECK_PATH =
296-
'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'
297-
298275
interface PermissionFeature {
299276
name: string
300277
key: string
@@ -377,8 +354,6 @@ function AccessControlPanel() {
377354
const isInView = useInView(ref, { once: true, margin: '-40px' })
378355
const [accessState, setAccessState] = useState<Record<string, boolean>>(INITIAL_ACCESS_STATE)
379356

380-
const allFeatures = PERMISSION_CATEGORIES.flatMap((c) => c.features)
381-
382357
return (
383358
<div ref={ref}>
384359
<div className='lg:hidden'>

apps/sim/ee/sso/components/sso-form.tsx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from 'next/navigation'
77
import { Button, Input, Label } from '@/components/emcn'
88
import { client } from '@/lib/auth/auth-client'
99
import { env, isFalsy } from '@/lib/core/config/env'
10+
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
1011
import { cn } from '@/lib/core/utils/cn'
1112
import { quickValidateEmail } from '@/lib/messaging/email/validation'
1213
import { BrandedButton } from '@/app/(auth)/components/branded-button'
@@ -29,24 +30,6 @@ const validateEmailField = (emailValue: string): string[] => {
2930
return errors
3031
}
3132

32-
const validateCallbackUrl = (url: string): boolean => {
33-
try {
34-
if (url.startsWith('/')) {
35-
return true
36-
}
37-
38-
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
39-
if (url.startsWith(currentOrigin)) {
40-
return true
41-
}
42-
43-
return false
44-
} catch (error) {
45-
logger.error('Error validating callback URL:', { error, url })
46-
return false
47-
}
48-
}
49-
5033
export default function SSOForm() {
5134
const router = useRouter()
5235
const searchParams = useSearchParams()
@@ -115,7 +98,7 @@ export default function SSOForm() {
11598
}
11699

117100
try {
118-
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/workspace'
101+
const safeCallbackUrl = callbackUrl
119102

120103
await client.signIn.sso({
121104
email: emailValue,

apps/sim/lib/core/security/input-validation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,3 +1169,26 @@ export function validatePaginationCursor(
11691169

11701170
return { isValid: true, sanitized: value }
11711171
}
1172+
1173+
/**
1174+
* Validates a callback URL to prevent open redirect attacks.
1175+
* Accepts relative paths and absolute URLs matching the current origin.
1176+
*
1177+
* @param url - The callback URL to validate
1178+
* @returns true if the URL is safe to redirect to
1179+
*/
1180+
export function validateCallbackUrl(url: string): boolean {
1181+
try {
1182+
if (url.startsWith('/')) return true
1183+
1184+
if (typeof window === 'undefined') return false
1185+
1186+
const currentOrigin = window.location.origin
1187+
if (url.startsWith(currentOrigin)) return true
1188+
1189+
return false
1190+
} catch (error) {
1191+
logger.error('Error validating callback URL:', { error, url })
1192+
return false
1193+
}
1194+
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ const DISPOSABLE_MX_BACKENDS = new Set(['in.mail.gw', 'smtp.catchmail.io', 'mx.y
6464

6565
/** Per-domain MX result cache — avoids redundant DNS queries for concurrent or repeated sign-ups */
6666
const mxCache = new Map<string, { result: boolean; expires: number }>()
67+
const MX_CACHE_MAX = 1_000
68+
69+
function setMxCache(domain: string, entry: { result: boolean; expires: number }) {
70+
if (mxCache.size >= MX_CACHE_MAX && !mxCache.has(domain)) {
71+
mxCache.delete(mxCache.keys().next().value!)
72+
}
73+
mxCache.set(domain, entry)
74+
}
6775

6876
/**
6977
* Validates email syntax using RFC 5322 compliant regex
@@ -124,7 +132,10 @@ export async function isDisposableMxBackend(email: string): Promise<boolean> {
124132

125133
const now = Date.now()
126134
const cached = mxCache.get(domain)
127-
if (cached && cached.expires > now) return cached.result
135+
if (cached) {
136+
if (cached.expires > now) return cached.result
137+
mxCache.delete(domain)
138+
}
128139

129140
let timeoutId: ReturnType<typeof setTimeout> | undefined
130141
try {
@@ -135,10 +146,10 @@ export async function isDisposableMxBackend(email: string): Promise<boolean> {
135146
}
136147
)
137148
const result = await Promise.race([mxCheckPromise, timeoutPromise])
138-
mxCache.set(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 })
149+
setMxCache(domain, { result: result.isDisposableBackend, expires: now + 5 * 60 * 1000 })
139150
return result.isDisposableBackend
140151
} catch {
141-
mxCache.set(domain, { result: false, expires: now + 60 * 1000 })
152+
setMxCache(domain, { result: false, expires: now + 60 * 1000 })
142153
return false
143154
} finally {
144155
clearTimeout(timeoutId)

0 commit comments

Comments
 (0)