Skip to content

Commit 274ebdf

Browse files
committed
fix(invite-workspace): addressed comments
1 parent 43cb4cb commit 274ebdf

File tree

4 files changed

+92
-10
lines changed

4 files changed

+92
-10
lines changed

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,33 @@ import { NotificationList } from '@/app/w/[id]/components/notifications/notifica
2525

2626
const logger = createLogger('LoginForm')
2727

28+
// Validate callback URL to prevent open redirect vulnerabilities
29+
const validateCallbackUrl = (url: string): boolean => {
30+
try {
31+
// If it's a relative URL, it's safe
32+
if (url.startsWith('/')) {
33+
return true
34+
}
35+
36+
// If absolute URL, check if it belongs to the same origin
37+
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : ''
38+
if (url.startsWith(currentOrigin)) {
39+
return true
40+
}
41+
42+
// Add other trusted domains if needed
43+
// const trustedDomains = ['trusted-domain.com']
44+
// if (trustedDomains.some(domain => url.startsWith(`https://${domain}`))) {
45+
// return true
46+
// }
47+
48+
return false
49+
} catch (error) {
50+
logger.error('Error validating callback URL:', { error, url })
51+
return false
52+
}
53+
}
54+
2855
export default function LoginPage({
2956
githubAvailable,
3057
googleAvailable,
@@ -63,7 +90,13 @@ export default function LoginPage({
6390
if (searchParams) {
6491
const callback = searchParams.get('callbackUrl')
6592
if (callback) {
66-
setCallbackUrl(callback)
93+
// Validate the callbackUrl before setting it
94+
if (validateCallbackUrl(callback)) {
95+
setCallbackUrl(callback)
96+
} else {
97+
logger.warn('Invalid callback URL detected and blocked:', { url: callback })
98+
// Keep the default safe value ('/w')
99+
}
67100
}
68101

69102
const inviteFlow = searchParams.get('invite_flow') === 'true'
@@ -79,12 +112,14 @@ export default function LoginPage({
79112
const email = formData.get('email') as string
80113

81114
try {
82-
// Use the extracted callbackUrl instead of hardcoded value
115+
// Final validation before submission
116+
const safeCallbackUrl = validateCallbackUrl(callbackUrl) ? callbackUrl : '/w'
117+
83118
const result = await client.signIn.email(
84119
{
85120
email,
86121
password,
87-
callbackURL: callbackUrl,
122+
callbackURL: safeCallbackUrl,
88123
},
89124
{
90125
onError: (ctx) => {

apps/sim/app/api/workflows/sync/route.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,37 @@ const SyncPayloadSchema = z.object({
5050
// Cache for workspace membership to reduce DB queries
5151
const workspaceMembershipCache = new Map<string, { role: string, expires: number }>();
5252
const CACHE_TTL = 60000; // 1 minute cache expiration
53+
const MAX_CACHE_SIZE = 1000; // Maximum number of entries to prevent unbounded growth
54+
55+
/**
56+
* Cleans up expired entries from the workspace membership cache
57+
*/
58+
function cleanupExpiredCacheEntries(): void {
59+
const now = Date.now();
60+
let expiredCount = 0;
61+
62+
// Remove expired entries
63+
for (const [key, value] of workspaceMembershipCache.entries()) {
64+
if (value.expires <= now) {
65+
workspaceMembershipCache.delete(key);
66+
expiredCount++;
67+
}
68+
}
69+
70+
// If we're still over the limit after removing expired entries,
71+
// remove the oldest entries (those that will expire soonest)
72+
if (workspaceMembershipCache.size > MAX_CACHE_SIZE) {
73+
const entries = Array.from(workspaceMembershipCache.entries())
74+
.sort((a, b) => a[1].expires - b[1].expires);
75+
76+
const toRemove = entries.slice(0, workspaceMembershipCache.size - MAX_CACHE_SIZE);
77+
toRemove.forEach(([key]) => workspaceMembershipCache.delete(key));
78+
79+
logger.debug(`Cache cleanup: removed ${expiredCount} expired entries and ${toRemove.length} additional entries due to size limit`);
80+
} else if (expiredCount > 0) {
81+
logger.debug(`Cache cleanup: removed ${expiredCount} expired entries`);
82+
}
83+
}
5384

5485
/**
5586
* Efficiently verifies user's membership and role in a workspace with caching
@@ -58,6 +89,11 @@ const CACHE_TTL = 60000; // 1 minute cache expiration
5889
* @returns Role if user is a member, null otherwise
5990
*/
6091
async function verifyWorkspaceMembership(userId: string, workspaceId: string): Promise<string | null> {
92+
// Opportunistic cleanup of expired cache entries
93+
if (workspaceMembershipCache.size > MAX_CACHE_SIZE / 2) {
94+
cleanupExpiredCacheEntries();
95+
}
96+
6197
// Create cache key from userId and workspaceId
6298
const cacheKey = `${userId}:${workspaceId}`;
6399

apps/sim/app/api/workspaces/invitations/accept/route.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,26 @@ export async function GET(req: NextRequest) {
4848
const userEmail = session.user.email.toLowerCase()
4949
const invitationEmail = invitation.email.toLowerCase()
5050

51-
// Check if invitation email matches the logged-in user
52-
// For new users who just signed up, we'll be more flexible by comparing domain parts
51+
// Check if the logged-in user's email matches the invitation
52+
// We'll use exact matching as the primary check
5353
const isExactMatch = userEmail === invitationEmail
54-
const isPartialMatch = userEmail.split('@')[1] === invitationEmail.split('@')[1] &&
55-
userEmail.split('@')[0].includes(invitationEmail.split('@')[0].substring(0, 3))
5654

57-
if (!isExactMatch && !isPartialMatch) {
55+
// For SSO or company email variants, check domain and normalized username
56+
// This handles cases like john.doe@company.com vs john@company.com
57+
const normalizeUsername = (email: string): string => {
58+
return email.split('@')[0].replace(/[^a-zA-Z0-9]/g, '').toLowerCase()
59+
}
60+
61+
const isSameDomain = userEmail.split('@')[1] === invitationEmail.split('@')[1]
62+
const normalizedUserEmail = normalizeUsername(userEmail)
63+
const normalizedInvitationEmail = normalizeUsername(invitationEmail)
64+
const isSimilarUsername = normalizedUserEmail === normalizedInvitationEmail ||
65+
(normalizedUserEmail.includes(normalizedInvitationEmail) ||
66+
normalizedInvitationEmail.includes(normalizedUserEmail))
67+
68+
const isValidMatch = isExactMatch || (isSameDomain && isSimilarUsername)
69+
70+
if (!isValidMatch) {
5871
// Get user info to include in the error message
5972
const userData = await db
6073
.select()

apps/sim/app/invite/[id]/invite.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,6 @@ export default function Invite() {
146146
invitationId: inviteId,
147147
})
148148

149-
console.log('Invitation acceptance response:', response)
150-
151149
// Set the active organization to the one just joined
152150
const orgId =
153151
response.data?.invitation.organizationId || invitationDetails?.data?.organizationId

0 commit comments

Comments
 (0)