mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
fix(sanitization): added more input sanitization to tool routes (#2475)
* fix(sanitization): added more input sanitization to tool routes * ack PR comments
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -108,6 +109,14 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
if (folderId) {
|
||||
const folderIdValidation = validateAlphanumericId(folderId, 'folderId', 50)
|
||||
if (!folderIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid folderId`, { error: folderIdValidation.error })
|
||||
return NextResponse.json({ error: folderIdValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const qParts: string[] = ['trashed = false']
|
||||
if (folderId) {
|
||||
qParts.push(`'${escapeForDriveQuery(folderId)}' in parents`)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -50,6 +51,29 @@ export async function POST(request: NextRequest) {
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
|
||||
for (const labelId of labelIds) {
|
||||
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
|
||||
if (!labelIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: labelIdValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
|
||||
if (!messageIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: messageIdValidation.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const gmailResponse = await fetch(
|
||||
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -38,6 +39,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credential ID: ${credentialIdValidation.error}`)
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
let credentials = await db
|
||||
.select()
|
||||
.from(account)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
@@ -53,6 +54,29 @@ export async function POST(request: NextRequest) {
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
|
||||
for (const labelId of labelIds) {
|
||||
const labelIdValidation = validateAlphanumericId(labelId, 'labelId', 255)
|
||||
if (!labelIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid label ID: ${labelIdValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: labelIdValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const messageIdValidation = validateAlphanumericId(validatedData.messageId, 'messageId', 255)
|
||||
if (!messageIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid message ID: ${messageIdValidation.error}`)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: messageIdValidation.error },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const gmailResponse = await fetch(
|
||||
`${GMAIL_API_BASE}/messages/${validatedData.messageId}/modify`,
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateUUID } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -25,7 +26,6 @@ export async function GET(request: NextRequest) {
|
||||
logger.info(`[${requestId}] Google Calendar calendars request received`)
|
||||
|
||||
try {
|
||||
// Get the credential ID from the query params
|
||||
const { searchParams } = new URL(request.url)
|
||||
const credentialId = searchParams.get('credentialId')
|
||||
const workflowId = searchParams.get('workflowId') || undefined
|
||||
@@ -34,12 +34,25 @@ export async function GET(request: NextRequest) {
|
||||
logger.warn(`[${requestId}] Missing credentialId parameter`)
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialValidation = validateUUID(credentialId, 'credentialId')
|
||||
if (!credentialValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credentialId format`, { credentialId })
|
||||
return NextResponse.json({ error: credentialValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
const workflowValidation = validateUUID(workflowId, 'workflowId')
|
||||
if (!workflowValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId })
|
||||
return NextResponse.json({ error: workflowValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Refresh access token if needed using the utility function
|
||||
const accessToken = await refreshAccessTokenIfNeeded(
|
||||
credentialId,
|
||||
authz.credentialOwnerUserId,
|
||||
@@ -50,7 +63,6 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Fetch calendars from Google Calendar API
|
||||
logger.info(`[${requestId}] Fetching calendars from Google Calendar API`)
|
||||
const calendarResponse = await fetch(
|
||||
'https://www.googleapis.com/calendar/v3/users/me/calendarList',
|
||||
@@ -81,7 +93,6 @@ export async function GET(request: NextRequest) {
|
||||
const data = await calendarResponse.json()
|
||||
const calendars: CalendarListItem[] = data.items || []
|
||||
|
||||
// Sort calendars with primary first, then alphabetically
|
||||
calendars.sort((a, b) => {
|
||||
if (a.primary && !b.primary) return -1
|
||||
if (!a.primary && b.primary) return 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -23,6 +24,12 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Team ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const teamIdValidation = validateMicrosoftGraphId(teamId, 'Team ID')
|
||||
if (!teamIdValidation.isValid) {
|
||||
logger.warn('Invalid team ID provided', { teamId, error: teamIdValidation.error })
|
||||
return NextResponse.json({ error: teamIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
@@ -70,7 +77,6 @@ export async function POST(request: Request) {
|
||||
endpoint: `https://graph.microsoft.com/v1.0/teams/${teamId}/channels`,
|
||||
})
|
||||
|
||||
// Check for auth errors specifically
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -93,7 +99,6 @@ export async function POST(request: Request) {
|
||||
} catch (innerError) {
|
||||
logger.error('Error during API requests:', innerError)
|
||||
|
||||
// Check if it's an authentication error
|
||||
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
if (
|
||||
errorMessage.includes('auth') ||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -7,21 +8,35 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('TeamsChatsAPI')
|
||||
|
||||
// Helper function to get chat members and create a meaningful name
|
||||
/**
|
||||
* Helper function to get chat members and create a meaningful name
|
||||
*
|
||||
* @param chatId - Microsoft Teams chat ID to get display name for
|
||||
* @param accessToken - Access token for Microsoft Graph API
|
||||
* @param chatTopic - Optional existing chat topic
|
||||
* @returns A meaningful display name for the chat
|
||||
*/
|
||||
const getChatDisplayName = async (
|
||||
chatId: string,
|
||||
accessToken: string,
|
||||
chatTopic?: string
|
||||
): Promise<string> => {
|
||||
try {
|
||||
// If the chat already has a topic, use it
|
||||
const chatIdValidation = validateMicrosoftGraphId(chatId, 'chatId')
|
||||
if (!chatIdValidation.isValid) {
|
||||
logger.warn('Invalid chat ID in getChatDisplayName', {
|
||||
error: chatIdValidation.error,
|
||||
chatId: chatId.substring(0, 50),
|
||||
})
|
||||
return `Chat ${chatId.substring(0, 8)}...`
|
||||
}
|
||||
|
||||
if (chatTopic?.trim() && chatTopic !== 'null') {
|
||||
return chatTopic
|
||||
}
|
||||
|
||||
// Fetch chat members to create a meaningful name
|
||||
const membersResponse = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/chats/${chatId}/members`,
|
||||
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/members`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -35,27 +50,25 @@ const getChatDisplayName = async (
|
||||
const membersData = await membersResponse.json()
|
||||
const members = membersData.value || []
|
||||
|
||||
// Filter out the current user and get display names
|
||||
const memberNames = members
|
||||
.filter((member: any) => member.displayName && member.displayName !== 'Unknown')
|
||||
.map((member: any) => member.displayName)
|
||||
.slice(0, 3) // Limit to first 3 names to avoid very long names
|
||||
.slice(0, 3)
|
||||
|
||||
if (memberNames.length > 0) {
|
||||
if (memberNames.length === 1) {
|
||||
return memberNames[0] // 1:1 chat
|
||||
return memberNames[0]
|
||||
}
|
||||
if (memberNames.length === 2) {
|
||||
return memberNames.join(' & ') // 2-person group
|
||||
return memberNames.join(' & ')
|
||||
}
|
||||
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more` // Larger group
|
||||
return `${memberNames.slice(0, 2).join(', ')} & ${memberNames.length - 2} more`
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get a better name from recent messages
|
||||
try {
|
||||
const messagesResponse = await fetch(
|
||||
`https://graph.microsoft.com/v1.0/chats/${chatId}/messages?$top=10&$orderby=createdDateTime desc`,
|
||||
`https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(chatId)}/messages?$top=10&$orderby=createdDateTime desc`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -69,14 +82,12 @@ const getChatDisplayName = async (
|
||||
const messagesData = await messagesResponse.json()
|
||||
const messages = messagesData.value || []
|
||||
|
||||
// Look for chat rename events
|
||||
for (const message of messages) {
|
||||
if (message.eventDetail?.chatDisplayName) {
|
||||
return message.eventDetail.chatDisplayName
|
||||
}
|
||||
}
|
||||
|
||||
// Get unique sender names from recent messages as last resort
|
||||
const senderNames = [
|
||||
...new Set(
|
||||
messages
|
||||
@@ -103,7 +114,6 @@ const getChatDisplayName = async (
|
||||
)
|
||||
}
|
||||
|
||||
// Final fallback
|
||||
return `Chat ${chatId.split(':')[0] || chatId.substring(0, 8)}...`
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
@@ -146,7 +156,6 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Could not retrieve access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Now try to fetch the chats
|
||||
const response = await fetch('https://graph.microsoft.com/v1.0/me/chats', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -163,7 +172,6 @@ export async function POST(request: Request) {
|
||||
endpoint: 'https://graph.microsoft.com/v1.0/me/chats',
|
||||
})
|
||||
|
||||
// Check for auth errors specifically
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -179,7 +187,6 @@ export async function POST(request: Request) {
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Process chats with enhanced display names
|
||||
const chats = await Promise.all(
|
||||
data.value.map(async (chat: any) => ({
|
||||
id: chat.id,
|
||||
@@ -193,7 +200,6 @@ export async function POST(request: Request) {
|
||||
} catch (innerError) {
|
||||
logger.error('Error during API requests:', innerError)
|
||||
|
||||
// Check if it's an authentication error
|
||||
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
if (
|
||||
errorMessage.includes('auth') ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -36,6 +37,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateMicrosoftGraphId(credentialId, 'credentialId')
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Fetching credential`, { credentialId })
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
|
||||
@@ -33,6 +34,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateMicrosoftGraphId(credentialId, 'credentialId')
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
if (!credentials.length) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
@@ -48,7 +55,6 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Build URL for OneDrive folders
|
||||
let url = `https://graph.microsoft.com/v1.0/me/drive/root/children?$filter=folder ne null&$select=id,name,folder,webUrl,createdDateTime,lastModifiedDateTime&$top=50`
|
||||
|
||||
if (query) {
|
||||
@@ -71,7 +77,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const data = await response.json()
|
||||
const folders = (data.value || [])
|
||||
.filter((item: MicrosoftGraphDriveItem) => item.folder) // Only folders
|
||||
.filter((item: MicrosoftGraphDriveItem) => item.folder)
|
||||
.map((folder: MicrosoftGraphDriveItem) => ({
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
|
||||
import * as XLSX from 'xlsx'
|
||||
import { z } from 'zod'
|
||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
||||
import { validateMicrosoftGraphId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import {
|
||||
@@ -28,9 +29,9 @@ const ExcelValuesSchema = z.union([
|
||||
const OneDriveUploadSchema = z.object({
|
||||
accessToken: z.string().min(1, 'Access token is required'),
|
||||
fileName: z.string().min(1, 'File name is required'),
|
||||
file: z.any().optional(), // UserFile object (optional for blank Excel creation)
|
||||
file: z.any().optional(),
|
||||
folderId: z.string().optional().nullable(),
|
||||
mimeType: z.string().nullish(), // Accept string, null, or undefined
|
||||
mimeType: z.string().nullish(),
|
||||
values: ExcelValuesSchema.optional().nullable(),
|
||||
})
|
||||
|
||||
@@ -62,24 +63,19 @@ export async function POST(request: NextRequest) {
|
||||
let fileBuffer: Buffer
|
||||
let mimeType: string
|
||||
|
||||
// Check if we're creating a blank Excel file
|
||||
const isExcelCreation =
|
||||
validatedData.mimeType ===
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' && !validatedData.file
|
||||
|
||||
if (isExcelCreation) {
|
||||
// Create a blank Excel workbook
|
||||
|
||||
const workbook = XLSX.utils.book_new()
|
||||
const worksheet = XLSX.utils.aoa_to_sheet([[]])
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
|
||||
|
||||
// Generate XLSX file as buffer
|
||||
const xlsxBuffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
|
||||
fileBuffer = Buffer.from(xlsxBuffer)
|
||||
mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
} else {
|
||||
// Handle regular file upload
|
||||
const rawFile = validatedData.file
|
||||
|
||||
if (!rawFile) {
|
||||
@@ -108,7 +104,6 @@ export async function POST(request: NextRequest) {
|
||||
fileToProcess = rawFile
|
||||
}
|
||||
|
||||
// Convert to UserFile format
|
||||
let userFile
|
||||
try {
|
||||
userFile = processSingleFileToUserFile(fileToProcess, requestId, logger)
|
||||
@@ -138,7 +133,7 @@ export async function POST(request: NextRequest) {
|
||||
mimeType = userFile.type || 'application/octet-stream'
|
||||
}
|
||||
|
||||
const maxSize = 250 * 1024 * 1024 // 250MB
|
||||
const maxSize = 250 * 1024 * 1024
|
||||
if (fileBuffer.length > maxSize) {
|
||||
const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2)
|
||||
logger.warn(`[${requestId}] File too large: ${sizeMB}MB`)
|
||||
@@ -151,7 +146,6 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Ensure file name has an appropriate extension
|
||||
let fileName = validatedData.fileName
|
||||
const hasExtension = fileName.includes('.') && fileName.lastIndexOf('.') > 0
|
||||
|
||||
@@ -169,6 +163,17 @@ export async function POST(request: NextRequest) {
|
||||
const folderId = validatedData.folderId?.trim()
|
||||
|
||||
if (folderId && folderId !== '') {
|
||||
const folderIdValidation = validateMicrosoftGraphId(folderId, 'folderId')
|
||||
if (!folderIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid folder ID`, { error: folderIdValidation.error })
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: folderIdValidation.error,
|
||||
},
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(folderId)}:/${encodeURIComponent(fileName)}:/content`
|
||||
} else {
|
||||
uploadUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/root:/${encodeURIComponent(fileName)}:/content`
|
||||
@@ -197,14 +202,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
const fileData = await uploadResponse.json()
|
||||
|
||||
// If this is an Excel creation and values were provided, write them using the Excel API
|
||||
let excelWriteResult: any | undefined
|
||||
const shouldWriteExcelContent =
|
||||
isExcelCreation && Array.isArray(excelValues) && excelValues.length > 0
|
||||
|
||||
if (shouldWriteExcelContent) {
|
||||
try {
|
||||
// Create a workbook session to ensure reliability and persistence of changes
|
||||
let workbookSessionId: string | undefined
|
||||
const sessionResp = await fetch(
|
||||
`${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(fileData.id)}/workbook/createSession`,
|
||||
@@ -223,7 +226,6 @@ export async function POST(request: NextRequest) {
|
||||
workbookSessionId = sessionData?.id
|
||||
}
|
||||
|
||||
// Determine the first worksheet name
|
||||
let sheetName = 'Sheet1'
|
||||
try {
|
||||
const listUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent(
|
||||
@@ -272,7 +274,6 @@ export async function POST(request: NextRequest) {
|
||||
return paddedRow
|
||||
})
|
||||
|
||||
// Compute concise end range from A1 and matrix size (no network round-trip)
|
||||
const indexToColLetters = (index: number): string => {
|
||||
let n = index
|
||||
let s = ''
|
||||
@@ -313,7 +314,6 @@ export async function POST(request: NextRequest) {
|
||||
statusText: excelWriteResponse?.statusText,
|
||||
error: errorText,
|
||||
})
|
||||
// Do not fail the entire request; return upload success with write error details
|
||||
excelWriteResult = {
|
||||
success: false,
|
||||
error: `Excel write failed: ${excelWriteResponse?.statusText || 'unknown'}`,
|
||||
@@ -321,7 +321,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
} else {
|
||||
const writeData = await excelWriteResponse.json()
|
||||
// The Range PATCH returns a Range object; log address and values length
|
||||
const addr = writeData.address || writeData.addressLocal
|
||||
const v = writeData.values || []
|
||||
excelWriteResult = {
|
||||
@@ -333,7 +332,6 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to close the workbook session if one was created
|
||||
if (workbookSessionId) {
|
||||
try {
|
||||
const closeResp = await fetch(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -29,8 +30,13 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId')
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn('Invalid credentialId format', { error: credentialIdValidation.error })
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure we have a session for permission checks
|
||||
const sessionUserId = session?.user?.id || ''
|
||||
|
||||
if (!sessionUserId) {
|
||||
@@ -38,7 +44,6 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Resolve the credential owner to support collaborator-owned credentials
|
||||
const creds = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
if (!creds.length) {
|
||||
logger.warn('Credential not found', { credentialId })
|
||||
@@ -79,7 +84,6 @@ export async function GET(request: Request) {
|
||||
endpoint: 'https://graph.microsoft.com/v1.0/me/mailFolders',
|
||||
})
|
||||
|
||||
// Check for auth errors specifically
|
||||
if (response.status === 401) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
@@ -96,7 +100,6 @@ export async function GET(request: Request) {
|
||||
const data = await response.json()
|
||||
const folders = data.value || []
|
||||
|
||||
// Transform folders to match the expected format
|
||||
const transformedFolders = folders.map((folder: OutlookFolder) => ({
|
||||
id: folder.id,
|
||||
name: folder.displayName,
|
||||
@@ -111,7 +114,6 @@ export async function GET(request: Request) {
|
||||
} catch (innerError) {
|
||||
logger.error('Error during API requests:', innerError)
|
||||
|
||||
// Check if it's an authentication error
|
||||
const errorMessage = innerError instanceof Error ? innerError.message : String(innerError)
|
||||
if (
|
||||
errorMessage.includes('auth') ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import type { SharepointSite } from '@/tools/sharepoint/types'
|
||||
@@ -32,6 +33,12 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialIdValidation = validateAlphanumericId(credentialId, 'credentialId', 255)
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credential ID`, { error: credentialIdValidation.error })
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
if (!credentials.length) {
|
||||
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
|
||||
@@ -47,8 +54,6 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Failed to obtain valid access token' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Build URL for SharePoint sites
|
||||
// Use search=* to get all sites the user has access to, or search for specific query
|
||||
const searchQuery = query || '*'
|
||||
const url = `https://graph.microsoft.com/v1.0/sites?search=${encodeURIComponent(searchQuery)}&$select=id,name,displayName,webUrl,createdDateTime,lastModifiedDateTime&$top=50`
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -93,7 +94,6 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to channels the bot can access and format the response
|
||||
const channels = (data.channels || [])
|
||||
.filter((channel: SlackChannel) => {
|
||||
const canAccess = !channel.is_archived && (channel.is_member || !channel.is_private)
|
||||
@@ -106,6 +106,28 @@ export async function POST(request: Request) {
|
||||
|
||||
return canAccess
|
||||
})
|
||||
.filter((channel: SlackChannel) => {
|
||||
const validation = validateAlphanumericId(channel.id, 'channelId', 50)
|
||||
|
||||
if (!validation.isValid) {
|
||||
logger.warn('Invalid channel ID received from Slack API', {
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
error: validation.error,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!/^[CDG][A-Z0-9]+$/i.test(channel.id)) {
|
||||
logger.warn('Channel ID does not match Slack format', {
|
||||
channelId: channel.id,
|
||||
channelName: channel.name,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.map((channel: SlackChannel) => ({
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -20,13 +21,21 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const requestId = generateRequestId()
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
const { credential, workflowId, userId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (userId !== undefined && userId !== null) {
|
||||
const validation = validateAlphanumericId(userId, 'userId', 100)
|
||||
if (!validation.isValid) {
|
||||
logger.warn('Invalid Slack user ID', { userId, error: validation.error })
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
let accessToken: string
|
||||
const isBotToken = credential.startsWith('xoxb-')
|
||||
|
||||
@@ -63,6 +72,17 @@ export async function POST(request: Request) {
|
||||
logger.info('Using OAuth token for Slack API')
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const userData = await fetchSlackUser(accessToken, userId)
|
||||
const user = {
|
||||
id: userData.user.id,
|
||||
name: userData.user.name,
|
||||
real_name: userData.user.real_name || userData.user.name,
|
||||
}
|
||||
logger.info(`Successfully fetched Slack user: ${userId}`)
|
||||
return NextResponse.json({ user })
|
||||
}
|
||||
|
||||
const data = await fetchSlackUsers(accessToken)
|
||||
|
||||
const users = (data.members || [])
|
||||
@@ -87,6 +107,31 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSlackUser(accessToken: string, userId: string) {
|
||||
const url = new URL('https://slack.com/api/users.info')
|
||||
url.searchParams.append('user', userId)
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Slack API error: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || 'Failed to fetch user')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
async function fetchSlackUsers(accessToken: string) {
|
||||
const url = new URL('https://slack.com/api/users.list')
|
||||
url.searchParams.append('limit', '200')
|
||||
|
||||
@@ -3,6 +3,7 @@ import { account } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { validateEnum, validatePathSegment } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -11,7 +12,6 @@ export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('WealthboxItemsAPI')
|
||||
|
||||
// Interface for transformed Wealthbox items
|
||||
interface WealthboxItem {
|
||||
id: string
|
||||
name: string
|
||||
@@ -45,12 +45,23 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (type !== 'contact') {
|
||||
const credentialIdValidation = validatePathSegment(credentialId, {
|
||||
paramName: 'credentialId',
|
||||
maxLength: 100,
|
||||
allowHyphens: true,
|
||||
allowUnderscores: true,
|
||||
allowDots: false,
|
||||
})
|
||||
if (!credentialIdValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid credentialId format: ${credentialId}`)
|
||||
return NextResponse.json({ error: credentialIdValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = ['contact'] as const
|
||||
const typeValidation = validateEnum(type, ALLOWED_TYPES, 'type')
|
||||
if (!typeValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid item type: ${type}`)
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid item type. Only contact is supported.' },
|
||||
{ status: 400 }
|
||||
)
|
||||
return NextResponse.json({ error: typeValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentials = await db.select().from(account).where(eq(account.id, credentialId)).limit(1)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -12,13 +13,21 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const requestId = generateRequestId()
|
||||
const body = await request.json()
|
||||
const { credential, workflowId } = body
|
||||
const { credential, workflowId, siteId } = body
|
||||
|
||||
if (!credential) {
|
||||
logger.error('Missing credential in request')
|
||||
return NextResponse.json({ error: 'Credential is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
const siteIdValidation = validateAlphanumericId(siteId, 'siteId')
|
||||
if (!siteIdValidation.isValid) {
|
||||
logger.error('Invalid siteId', { error: siteIdValidation.error })
|
||||
return NextResponse.json({ error: siteIdValidation.error }, { status: 400 })
|
||||
}
|
||||
}
|
||||
|
||||
const authz = await authorizeCredentialUse(request as any, {
|
||||
credentialId: credential,
|
||||
workflowId,
|
||||
@@ -46,7 +55,11 @@ export async function POST(request: Request) {
|
||||
)
|
||||
}
|
||||
|
||||
const response = await fetch('https://api.webflow.com/v2/sites', {
|
||||
const url = siteId
|
||||
? `https://api.webflow.com/v2/sites/${siteId}`
|
||||
: 'https://api.webflow.com/v2/sites'
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
accept: 'application/json',
|
||||
@@ -58,6 +71,7 @@ export async function POST(request: Request) {
|
||||
logger.error('Failed to fetch Webflow sites', {
|
||||
status: response.status,
|
||||
error: errorData,
|
||||
siteId: siteId || 'all',
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch Webflow sites', details: errorData },
|
||||
@@ -66,7 +80,13 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const sites = data.sites || []
|
||||
|
||||
let sites: any[]
|
||||
if (siteId) {
|
||||
sites = [data]
|
||||
} else {
|
||||
sites = data.sites || []
|
||||
}
|
||||
|
||||
const formattedSites = sites.map((site: any) => ({
|
||||
id: site.id,
|
||||
|
||||
@@ -958,3 +958,112 @@ export function createPinnedUrl(originalUrl: string, resolvedIP: string): string
|
||||
const port = parsed.port ? `:${parsed.port}` : ''
|
||||
return `${parsed.protocol}//${resolvedIP}${port}${parsed.pathname}${parsed.search}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Google Calendar ID
|
||||
*
|
||||
* Google Calendar IDs can be:
|
||||
* - "primary" (literal string for the user's primary calendar)
|
||||
* - Email addresses (for user calendars)
|
||||
* - Alphanumeric strings with hyphens, underscores, and dots (for other calendars)
|
||||
*
|
||||
* This validator allows these legitimate formats while blocking path traversal and injection attempts.
|
||||
*
|
||||
* @param value - The calendar ID to validate
|
||||
* @param paramName - Name of the parameter for error messages
|
||||
* @returns ValidationResult
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = validateGoogleCalendarId(calendarId, 'calendarId')
|
||||
* if (!result.isValid) {
|
||||
* return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function validateGoogleCalendarId(
|
||||
value: string | null | undefined,
|
||||
paramName = 'calendarId'
|
||||
): ValidationResult {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} is required`,
|
||||
}
|
||||
}
|
||||
|
||||
if (value === 'primary') {
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
const pathTraversalPatterns = [
|
||||
'../',
|
||||
'..\\',
|
||||
'%2e%2e%2f',
|
||||
'%2e%2e/',
|
||||
'..%2f',
|
||||
'%2e%2e%5c',
|
||||
'%2e%2e\\',
|
||||
'..%5c',
|
||||
'%252e%252e%252f',
|
||||
]
|
||||
|
||||
const lowerValue = value.toLowerCase()
|
||||
for (const pattern of pathTraversalPatterns) {
|
||||
if (lowerValue.includes(pattern)) {
|
||||
logger.warn('Path traversal attempt in Google Calendar ID', {
|
||||
paramName,
|
||||
value: value.substring(0, 100),
|
||||
})
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} contains invalid path traversal sequence`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) {
|
||||
logger.warn('Control characters in Google Calendar ID', { paramName })
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} contains invalid control characters`,
|
||||
}
|
||||
}
|
||||
|
||||
if (value.includes('\n') || value.includes('\r')) {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} contains invalid newline characters`,
|
||||
}
|
||||
}
|
||||
|
||||
const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
if (emailPattern.test(value)) {
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
const calendarIdPattern = /^[a-zA-Z0-9._@%#+-]+$/
|
||||
if (!calendarIdPattern.test(value)) {
|
||||
logger.warn('Invalid Google Calendar ID format', {
|
||||
paramName,
|
||||
value: value.substring(0, 100),
|
||||
})
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} format is invalid. Must be "primary", an email address, or an alphanumeric ID`,
|
||||
}
|
||||
}
|
||||
|
||||
if (value.length > 255) {
|
||||
logger.warn('Google Calendar ID exceeds maximum length', {
|
||||
paramName,
|
||||
length: value.length,
|
||||
})
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} exceeds maximum length of 255 characters`,
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user