Skip to content

Commit 0b43d6f

Browse files
committed
fix(auth): added same-origin validation to forget password route, added confirmation for disable auth FF
1 parent 7ef1150 commit 0b43d6f

File tree

4 files changed

+82
-4
lines changed

4 files changed

+82
-4
lines changed

apps/sim/app/api/auth/forget-password/route.test.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
77
import { createMockRequest, setupAuthApiMocks } from '@/app/api/__test-utils__/utils'
88

9+
vi.mock('@/lib/core/config/env', () => ({
10+
getEnv: vi.fn((key: string) => {
11+
if (key === 'NEXT_PUBLIC_APP_URL') {
12+
return 'https://app.example.com'
13+
}
14+
return undefined
15+
}),
16+
}))
17+
918
describe('Forget Password API Route', () => {
1019
beforeEach(() => {
1120
vi.resetModules()
@@ -15,7 +24,7 @@ describe('Forget Password API Route', () => {
1524
vi.clearAllMocks()
1625
})
1726

18-
it('should send password reset email successfully', async () => {
27+
it('should send password reset email successfully with same-origin redirectTo', async () => {
1928
setupAuthApiMocks({
2029
operations: {
2130
forgetPassword: { success: true },
@@ -24,7 +33,7 @@ describe('Forget Password API Route', () => {
2433

2534
const req = createMockRequest('POST', {
2635
email: 'test@example.com',
27-
redirectTo: 'https://example.com/reset',
36+
redirectTo: 'https://app.example.com/reset',
2837
})
2938

3039
const { POST } = await import('@/app/api/auth/forget-password/route')
@@ -39,12 +48,36 @@ describe('Forget Password API Route', () => {
3948
expect(auth.auth.api.forgetPassword).toHaveBeenCalledWith({
4049
body: {
4150
email: 'test@example.com',
42-
redirectTo: 'https://example.com/reset',
51+
redirectTo: 'https://app.example.com/reset',
4352
},
4453
method: 'POST',
4554
})
4655
})
4756

57+
it('should reject external redirectTo URL', async () => {
58+
setupAuthApiMocks({
59+
operations: {
60+
forgetPassword: { success: true },
61+
},
62+
})
63+
64+
const req = createMockRequest('POST', {
65+
email: 'test@example.com',
66+
redirectTo: 'https://evil.com/phishing',
67+
})
68+
69+
const { POST } = await import('@/app/api/auth/forget-password/route')
70+
71+
const response = await POST(req)
72+
const data = await response.json()
73+
74+
expect(response.status).toBe(400)
75+
expect(data.message).toBe('Redirect URL must be same-origin')
76+
77+
const auth = await import('@/lib/auth')
78+
expect(auth.auth.api.forgetPassword).not.toHaveBeenCalled()
79+
})
80+
4881
it('should send password reset email without redirectTo', async () => {
4982
setupAuthApiMocks({
5083
operations: {

apps/sim/app/api/auth/forget-password/route.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { z } from 'zod'
33
import { auth } from '@/lib/auth'
4+
import { isSameOrigin } from '@/lib/core/utils/validation'
45
import { createLogger } from '@/lib/logs/console/logger'
56

67
export const dynamic = 'force-dynamic'
@@ -14,6 +15,9 @@ const forgetPasswordSchema = z.object({
1415
redirectTo: z
1516
.string()
1617
.url('Redirect URL must be a valid URL')
18+
.refine((url) => isSameOrigin(url), {
19+
message: 'Redirect URL must be same-origin',
20+
})
1721
.optional()
1822
.or(z.literal(''))
1923
.transform((val) => (val === '' ? undefined : val)),

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
/**
22
* Environment utility functions for consistent environment detection across the application
33
*/
4+
5+
import { createLogger } from '@/lib/logs/console/logger'
46
import { env, getEnv, isTruthy } from './env'
57

8+
const logger = createLogger('FeatureFlags')
9+
610
/**
711
* Is the application running in production mode
812
*/
@@ -37,8 +41,22 @@ export const isEmailVerificationEnabled = isTruthy(env.EMAIL_VERIFICATION_ENABLE
3741

3842
/**
3943
* Is authentication disabled (for self-hosted deployments behind private networks)
44+
* This flag is blocked when isHosted is true.
4045
*/
41-
export const isAuthDisabled = isTruthy(env.DISABLE_AUTH)
46+
export const isAuthDisabled = isTruthy(env.DISABLE_AUTH) && !isHosted
47+
48+
if (isTruthy(env.DISABLE_AUTH)) {
49+
if (isHosted) {
50+
logger.error(
51+
'DISABLE_AUTH is set but ignored on hosted environment. Authentication remains enabled for security.'
52+
)
53+
} else {
54+
logger.warn(
55+
'DISABLE_AUTH is enabled. Authentication is bypassed and all requests use an anonymous session. ' +
56+
'Only use this in trusted private networks.'
57+
)
58+
}
59+
}
4260

4361
/**
4462
* Is user registration disabled

apps/sim/lib/core/utils/validation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,26 @@
1+
import { getEnv } from '@/lib/core/config/env'
2+
3+
/**
4+
* Checks if a URL is same-origin with the application's base URL.
5+
* Used to prevent open redirect vulnerabilities.
6+
*
7+
* @param url - The URL to validate
8+
* @returns True if the URL is same-origin, false otherwise (secure default)
9+
*/
10+
export function isSameOrigin(url: string): boolean {
11+
try {
12+
const appBaseUrl = getEnv('NEXT_PUBLIC_APP_URL')
13+
if (!appBaseUrl) {
14+
return false
15+
}
16+
const targetUrl = new URL(url)
17+
const appUrl = new URL(appBaseUrl)
18+
return targetUrl.origin === appUrl.origin
19+
} catch {
20+
return false
21+
}
22+
}
23+
124
/**
225
* Validates a name by removing any characters that could cause issues
326
* with variable references or node naming.

0 commit comments

Comments
 (0)