Skip to content

Commit 8f97b0a

Browse files
committed
fix(chat): reject whitespace-only passwords
1 parent 36022ce commit 8f97b0a

5 files changed

Lines changed: 44 additions & 3 deletions

File tree

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,24 @@ describe('Chat Edit API Route', () => {
429429
expect(data.error).toBe('Password is required when using password protection')
430430
})
431431

432+
it('rejects a whitespace-only replacement password', async () => {
433+
authMockFns.mockGetSession.mockResolvedValue({
434+
user: { id: 'user-id' },
435+
})
436+
437+
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
438+
method: 'PATCH',
439+
body: JSON.stringify({ authType: 'password', password: ' ' }),
440+
})
441+
const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) })
442+
443+
expect(response.status).toBe(400)
444+
const data = await response.json()
445+
expect(data.error).toBe('Password cannot contain only whitespace')
446+
expect(mockCheckChatAccess).not.toHaveBeenCalled()
447+
expect(mockEncryptSecret).not.toHaveBeenCalled()
448+
})
449+
432450
it('should keep the existing password when updating a password-protected chat', async () => {
433451
authMockFns.mockGetSession.mockResolvedValue({
434452
user: { id: 'user-id' },

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
getPasswordPlaceholder,
4343
hasExistingPassword,
4444
isPasswordRequired,
45+
isWhitespaceOnlyPassword,
4546
shouldConfirmPasswordChange,
4647
} from './utils'
4748

@@ -158,6 +159,8 @@ export function ChatDeploy({
158159

159160
if (isPasswordRequired(formData.authType, formData.password, existingPassword)) {
160161
newErrors.password = 'Password is required when using password protection'
162+
} else if (formData.authType === 'password' && isWhitespaceOnlyPassword(formData.password)) {
163+
newErrors.password = 'Password cannot contain only whitespace'
161164
}
162165

163166
if (
@@ -180,6 +183,7 @@ export function ChatDeploy({
180183
Boolean(formData.title.trim()) &&
181184
formData.selectedOutputBlocks.length > 0 &&
182185
!isPasswordRequired(formData.authType, formData.password, existingPassword) &&
186+
(formData.authType !== 'password' || !isWhitespaceOnlyPassword(formData.password)) &&
183187
((formData.authType !== 'email' && formData.authType !== 'sso') || formData.emails.length > 0)
184188

185189
useEffect(() => {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getPasswordPlaceholder,
55
hasExistingPassword,
66
isPasswordRequired,
7+
isWhitespaceOnlyPassword,
78
shouldConfirmPasswordChange,
89
} from './utils'
910

@@ -24,6 +25,12 @@ describe.concurrent('chat password state', () => {
2425
expect(isPasswordRequired('password', '', true)).toBe(false)
2526
})
2627

28+
it('identifies whitespace-only password values', () => {
29+
expect(isWhitespaceOnlyPassword(' ')).toBe(true)
30+
expect(isWhitespaceOnlyPassword('')).toBe(false)
31+
expect(isWhitespaceOnlyPassword(' password ')).toBe(false)
32+
})
33+
2734
it('returns copy that matches the stored-password state', () => {
2835
expect(getPasswordPlaceholder(true)).toBe('Enter new password to change')
2936
expect(getPasswordHelperText(true)).toBe('Leave empty to keep the current password')
@@ -34,6 +41,7 @@ describe.concurrent('chat password state', () => {
3441
it('confirms a password change only for an existing password deployment with a new value', () => {
3542
expect(shouldConfirmPasswordChange(true, 'password', 'new-password')).toBe(true)
3643
expect(shouldConfirmPasswordChange(true, 'password', '')).toBe(false)
44+
expect(shouldConfirmPasswordChange(true, 'password', ' ')).toBe(false)
3745
expect(shouldConfirmPasswordChange(true, 'public', 'new-password')).toBe(false)
3846
expect(shouldConfirmPasswordChange(false, 'password', 'new-password')).toBe(false)
3947
})

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/utils.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ export function isPasswordRequired(
1919
return authType === 'password' && !existingPassword && !password.trim()
2020
}
2121

22+
export function isWhitespaceOnlyPassword(password: string): boolean {
23+
return password.length > 0 && password.trim().length === 0
24+
}
25+
2226
export function shouldConfirmPasswordChange(
2327
hasExistingChat: boolean,
2428
authType: AuthType,
2529
password: string
2630
): boolean {
27-
return hasExistingChat && authType === 'password' && password.length > 0
31+
return hasExistingChat && authType === 'password' && password.trim().length > 0
2832
}
2933

3034
export function getPasswordPlaceholder(existingPassword: boolean): string {

apps/sim/lib/api/contracts/chats.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import { defineRouteContract } from '@/lib/api/contracts/types'
44
export const chatAuthTypeSchema = z.enum(['public', 'password', 'email', 'sso'])
55
export type ChatAuthType = z.output<typeof chatAuthTypeSchema>
66

7+
export const chatDeploymentPasswordSchema = z
8+
.string()
9+
.refine(
10+
(password) => password.length === 0 || password.trim().length > 0,
11+
'Password cannot contain only whitespace'
12+
)
13+
714
export const chatIdParamsSchema = z.object({
815
id: z.string().min(1),
916
})
@@ -38,7 +45,7 @@ export const createChatBodySchema = z.object({
3845
description: z.string().optional(),
3946
customizations: chatCustomizationsSchema,
4047
authType: chatAuthTypeSchema.default('public'),
41-
password: z.string().optional(),
48+
password: chatDeploymentPasswordSchema.optional(),
4249
allowedEmails: z.array(z.string()).optional().default([]),
4350
outputConfigs: z.array(chatOutputConfigSchema).optional().default([]),
4451
/** When true, clients may receive thinking SSE if they also send the protocol header. Default off. */
@@ -59,7 +66,7 @@ export const updateChatBodySchema = z.object({
5966
description: z.string().optional(),
6067
customizations: chatCustomizationsSchema.optional(),
6168
authType: chatAuthTypeSchema.optional(),
62-
password: z.string().optional(),
69+
password: chatDeploymentPasswordSchema.optional(),
6370
allowedEmails: z.array(z.string()).optional(),
6471
outputConfigs: z.array(chatOutputConfigSchema).optional(),
6572
includeThinking: z.boolean().optional(),

0 commit comments

Comments
 (0)