Skip to content

Commit 656a6b8

Browse files
authored
fix(sanitization): added more input sanitization to tool routes (#2475)
* fix(sanitization): added more input sanitization to tool routes * ack PR comments
1 parent 889b44c commit 656a6b8

File tree

17 files changed

+372
-61
lines changed

17 files changed

+372
-61
lines changed

apps/sim/app/api/tools/drive/files/route.ts

Lines changed: 9 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 { getSession } from '@/lib/auth'
33
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
4+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
45
import { generateRequestId } from '@/lib/core/utils/request'
56
import { createLogger } from '@/lib/logs/console/logger'
67
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -108,6 +109,14 @@ export async function GET(request: NextRequest) {
108109
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
109110
}
110111

112+
if (folderId) {
113+
const folderIdValidation = validateAlphanumericId(folderId, 'folderId', 50)
114+
if (!folderIdValidation.isValid) {
115+
logger.warn(`[${requestId}] Invalid folderId`, { error: folderIdValidation.error })
116+
return NextResponse.json({ error: folderIdValidation.error }, { status: 400 })
117+
}
118+
}
119+
111120
const qParts: string[] = ['trashed = false']
112121
if (folderId) {
113122
qParts.push(`'${escapeForDriveQuery(folderId)}' in parents`)

apps/sim/app/api/tools/gmail/add-label/route.ts

Lines changed: 24 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 { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
45
import { generateRequestId } from '@/lib/core/utils/request'
56
import { createLogger } from '@/lib/logs/console/logger'
67

@@ -50,6 +51,29 @@ export async function POST(request: NextRequest) {
5051
.map((id) => id.trim())
5152
.filter((id) => id.length > 0)
5253

54+
for (const labelId of labelIds) {
55+
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
56+
if (!labelIdValidation.isValid) {
57+
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
58+
return NextResponse.json(
59+
{
60+
success: false,
61+
error: labelIdValidation.error,
62+
},
63+
{ status: 400 }
64+
)
65+
}
66+
}
67+
68+
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
69+
if (!messageIdValidation.isValid) {
70+
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
71+
return NextResponse.json(
72+
{ success: false, error: messageIdValidation.error },
73+
{ status: 400 }
74+
)
75+
}
76+
5377
const gmailResponse = await fetch(
5478
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
5579
{

apps/sim/app/api/tools/gmail/labels/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
33
import { and, eq } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
6+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { createLogger } from '@/lib/logs/console/logger'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -38,6 +39,12 @@ export async function GET(request: NextRequest) {
3839
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3940
}
4041

42+
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
43+
if (!credentialIdValidation.isValid) {
44+
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
45+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
46+
}
47+
4148
let credentials = await db
4249
.select()
4350
.from(account)

apps/sim/app/api/tools/gmail/remove-label/route.ts

Lines changed: 24 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 { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
45
import { generateRequestId } from '@/lib/core/utils/request'
56
import { createLogger } from '@/lib/logs/console/logger'
67

@@ -53,6 +54,29 @@ export async function POST(request: NextRequest) {
5354
.map((id) => id.trim())
5455
.filter((id) => id.length > 0)
5556

57+
for (const labelId of labelIds) {
58+
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
59+
if (!labelIdValidation.isValid) {
60+
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
61+
return NextResponse.json(
62+
{
63+
success: false,
64+
error: labelIdValidation.error,
65+
},
66+
{ status: 400 }
67+
)
68+
}
69+
}
70+
71+
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
72+
if (!messageIdValidation.isValid) {
73+
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
74+
return NextResponse.json(
75+
{ success: false, error: messageIdValidation.error },
76+
{ status: 400 }
77+
)
78+
}
79+
5680
const gmailResponse = await fetch(
5781
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
5882
{

apps/sim/app/api/tools/google_calendar/calendars/route.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { validateUUID } from '@/lib/core/security/input-validation'
34
import { generateRequestId } from '@/lib/core/utils/request'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -25,7 +26,6 @@ export async function GET(request: NextRequest) {
2526
logger.info(`[${requestId}] Google Calendar calendars request received`)
2627

2728
try {
28-
// Get the credential ID from the query params
2929
const { searchParams } = new URL(request.url)
3030
const credentialId = searchParams.get('credentialId')
3131
const workflowId = searchParams.get('workflowId') || undefined
@@ -34,12 +34,25 @@ export async function GET(request: NextRequest) {
3434
logger.warn(`[${requestId}] Missing credentialId parameter`)
3535
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3636
}
37+
38+
const credentialValidation = validateUUID(credentialId, 'credentialId')
39+
if (!credentialValidation.isValid) {
40+
logger.warn(`[${requestId}] Invalid credentialId format`, { credentialId })
41+
return NextResponse.json({ error: credentialValidation.error }, { status: 400 })
42+
}
43+
44+
if (workflowId) {
45+
const workflowValidation = validateUUID(workflowId, 'workflowId')
46+
if (!workflowValidation.isValid) {
47+
logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId })
48+
return NextResponse.json({ error: workflowValidation.error }, { status: 400 })
49+
}
50+
}
3751
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
3852
if (!authz.ok || !authz.credentialOwnerUserId) {
3953
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
4054
}
4155

42-
// Refresh access token if needed using the utility function
4356
const accessToken = await refreshAccessTokenIfNeeded(
4457
credentialId,
4558
authz.credentialOwnerUserId,
@@ -50,7 +63,6 @@ export async function GET(request: NextRequest) {
5063
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
5164
}
5265

53-
// Fetch calendars from Google Calendar API
5466
logger.info(`[${requestId}] Fetching calendars from Google Calendar API`)
5567
const calendarResponse = await fetch(
5668
'https://www.googleapis.com/calendar/v3/users/me/calendarList',
@@ -81,7 +93,6 @@ export async function GET(request: NextRequest) {
8193
const data = await calendarResponse.json()
8294
const calendars: CalendarListItem[] = data.items || []
8395

84-
// Sort calendars with primary first, then alphabetically
8596
calendars.sort((a, b) => {
8697
if (a.primary && !b.primary) return -1
8798
if (!a.primary && b.primary) return 1

apps/sim/app/api/tools/microsoft-teams/channels/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { NextResponse } from 'next/server'
22
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
34
import { createLogger } from '@/lib/logs/console/logger'
45
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
56

@@ -23,6 +24,12 @@ export async function POST(request: Request) {
2324
return NextResponse.json({ error: 'Team ID is required' }, { status: 400 })
2425
}
2526

27+
const teamIdValidation = validateMicrosoftGraphId(teamId, 'Team ID')
28+
if (!teamIdValidation.isValid) {
29+
logger.warn('Invalid team ID provided', { teamId, error: teamIdValidation.error })
30+
return NextResponse.json({ error: teamIdValidation.error }, { status: 400 })
31+
}
32+
2633
try {
2734
const authz = await authorizeCredentialUse(request as any, {
2835
credentialId: credential,
@@ -70,7 +77,6 @@ export async function POST(request: Request) {
7077
endpoint: `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
7178
})
7279

73-
// Check for auth errors specifically
7480
if (response.status === 401) {
7581
return NextResponse.json(
7682
{
@@ -93,7 +99,6 @@ export async function POST(request: Request) {
9399
} catch (innerError) {
94100
logger.error('Error during API requests:', innerError)
95101

96-
// Check if it's an authentication error
97102
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
98103
if (
99104
errorMessage.includes('auth') ||

apps/sim/app/api/tools/microsoft-teams/chats/route.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,42 @@
11
import { NextResponse } from 'next/server'
22
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
3+
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
34
import { createLogger } from '@/lib/logs/console/logger'
45
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
56

67
export const dynamic = 'force-dynamic'
78

89
const logger = createLogger('TeamsChatsAPI')
910

10-
// Helper function to get chat members and create a meaningful name
11+
/**
12+
* Helper function to get chat members and create a meaningful name
13+
*
14+
* @param chatId - Microsoft Teams chat ID to get display name for
15+
* @param accessToken - Access token for Microsoft Graph API
16+
* @param chatTopic - Optional existing chat topic
17+
* @returns A meaningful display name for the chat
18+
*/
1119
const getChatDisplayName = async (
1220
chatId: string,
1321
accessToken: string,
1422
chatTopic?: string
1523
): Promise<string> => {
1624
try {
17-
// If the chat already has a topic, use it
25+
const chatIdValidation = validateMicrosoftGraphId(chatId, 'chatId')
26+
if (!chatIdValidation.isValid) {
27+
logger.warn('Invalid chat ID in getChatDisplayName', {
28+
error: chatIdValidation.error,
29+
chatId: chatId.substring(0, 50),
30+
})
31+
return `Chat ${chatId.substring(0, 8)}...`
32+
}
33+
1834
if (chatTopic?.trim() && chatTopic !== 'null') {
1935
return chatTopic
2036
}
2137

22-
// Fetch chat members to create a meaningful name
2338
const membersResponse = await fetch(
24-
`https://graph.microsoft.com/v1.0/chats/${chatId}/members`,
39+
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
2540
{
2641
method: 'GET',
2742
headers: {
@@ -35,27 +50,25 @@ const getChatDisplayName = async (
3550
const membersData = await membersResponse.json()
3651
const members = membersData.value || []
3752

38-
// Filter out the current user and get display names
3953
const memberNames = members
4054
.filter((member: any) => member.displayName && member.displayName !== 'Unknown')
4155
.map((member: any) => member.displayName)
42-
.slice(0, 3) // Limit to first 3 names to avoid very long names
56+
.slice(0, 3)
4357

4458
if (memberNames.length > 0) {
4559
if (memberNames.length === 1) {
46-
return memberNames[0] // 1:1 chat
60+
return memberNames[0]
4761
}
4862
if (memberNames.length === 2) {
49-
return memberNames.join(' & ') // 2-person group
63+
return memberNames.join(' & ')
5064
}
51-
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` // Larger group
65+
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more`
5266
}
5367
}
5468

55-
// Fallback: try to get a better name from recent messages
5669
try {
5770
const messagesResponse = await fetch(
58-
`https://graph.microsoft.com/v1.0/chats/${chatId}/messages?$top=10&$orderby=createdDateTime desc`,
71+
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=10&$orderby=createdDateTime desc`,
5972
{
6073
method: 'GET',
6174
headers: {
@@ -69,14 +82,12 @@ const getChatDisplayName = async (
6982
const messagesData = await messagesResponse.json()
7083
const messages = messagesData.value || []
7184

72-
// Look for chat rename events
7385
for (const message of messages) {
7486
if (message.eventDetail?.chatDisplayName) {
7587
return message.eventDetail.chatDisplayName
7688
}
7789
}
7890

79-
// Get unique sender names from recent messages as last resort
8091
const senderNames = [
8192
...new Set(
8293
messages
@@ -103,7 +114,6 @@ const getChatDisplayName = async (
103114
)
104115
}
105116

106-
// Final fallback
107117
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
108118
} catch (error) {
109119
logger.warn(
@@ -146,7 +156,6 @@ export async function POST(request: Request) {
146156
return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 })
147157
}
148158

149-
// Now try to fetch the chats
150159
const response = await fetch('https://graph.microsoft.com/v1.0/me/chats', {
151160
method: 'GET',
152161
headers: {
@@ -163,7 +172,6 @@ export async function POST(request: Request) {
163172
endpoint: 'https://graph.microsoft.com/v1.0/me/chats',
164173
})
165174

166-
// Check for auth errors specifically
167175
if (response.status === 401) {
168176
return NextResponse.json(
169177
{
@@ -179,7 +187,6 @@ export async function POST(request: Request) {
179187

180188
const data = await response.json()
181189

182-
// Process chats with enhanced display names
183190
const chats = await Promise.all(
184191
data.value.map(async (chat: any) => ({
185192
id: chat.id,
@@ -193,7 +200,6 @@ export async function POST(request: Request) {
193200
} catch (innerError) {
194201
logger.error('Error during API requests:', innerError)
195202

196-
// Check if it's an authentication error
197203
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
198204
if (
199205
errorMessage.includes('auth') ||

apps/sim/app/api/tools/onedrive/files/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
7+
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
78
import { createLogger } from '@/lib/logs/console/logger'
89
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
910

@@ -36,6 +37,12 @@ export async function GET(request: NextRequest) {
3637
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
3738
}
3839

40+
const credentialIdValidation = validateMicrosoftGraphId(credentialId, 'credentialId')
41+
if (!credentialIdValidation.isValid) {
42+
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
43+
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
44+
}
45+
3946
logger.info(`[${requestId}] Fetching credential`, { credentialId })
4047

4148
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)

0 commit comments

Comments
 (0)