From d71824c24ca609c404aa983d511b76eb38840006 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 14 May 2026 11:33:25 -0700 Subject: [PATCH 1/6] fix(security): harden file access controls, webhook auth, and input bounds --- apps/sim/app/api/files/authorization.ts | 6 +-- .../sim/app/api/tools/agiloft/attach/route.ts | 5 +- apps/sim/app/api/tools/box/upload/route.ts | 5 +- .../confluence/upload-attachment/route.ts | 9 ++++ .../api/tools/discord/send-message/route.ts | 35 ++++++++---- apps/sim/app/api/tools/docusign/route.ts | 10 ++-- .../sim/app/api/tools/dropbox/upload/route.ts | 5 +- .../app/api/tools/firecrawl/parse/route.ts | 4 ++ apps/sim/app/api/tools/gmail/draft/route.ts | 29 ++++++---- .../app/api/tools/gmail/edit-draft/route.ts | 35 +++++++++--- apps/sim/app/api/tools/gmail/send/route.ts | 29 ++++++---- .../api/tools/google_drive/upload/route.ts | 6 ++- .../api/tools/jira/add-attachment/route.ts | 5 +- .../microsoft-dataverse/upload-file/route.ts | 6 ++- apps/sim/app/api/tools/mistral/parse/route.ts | 3 ++ .../app/api/tools/onedrive/upload/route.ts | 6 ++- apps/sim/app/api/tools/outlook/draft/route.ts | 33 +++++++----- apps/sim/app/api/tools/outlook/send/route.ts | 33 +++++++----- .../api/tools/quiver/image-to-svg/route.ts | 17 +++++- apps/sim/app/api/tools/s3/put-object/route.ts | 6 ++- .../app/api/tools/sendgrid/send-mail/route.ts | 28 ++++++---- apps/sim/app/api/tools/sftp/upload/route.ts | 2 +- .../app/api/tools/sharepoint/upload/route.ts | 2 +- apps/sim/app/api/tools/smtp/send/route.ts | 51 ++++++++++-------- .../tools/supabase/storage-upload/route.ts | 5 +- .../api/tools/telegram/send-document/route.ts | 6 ++- apps/sim/app/api/tools/video/route.ts | 13 ++++- .../sim/app/api/tools/vision/analyze/route.ts | 10 +++- .../app/api/tools/wordpress/upload/route.ts | 2 +- .../resource-options-bar.tsx | 2 +- .../components/logs-toolbar/logs-toolbar.tsx | 16 ++++-- .../app/workspace/[workspaceId]/logs/logs.tsx | 14 +++-- .../components/date-picker/date-picker.tsx | 54 ++++++++++++++++--- .../sim/lib/api/contracts/storage-transfer.ts | 2 +- apps/sim/lib/logs/filters.ts | 4 +- apps/sim/lib/webhooks/providers/attio.ts | 12 +---- apps/sim/lib/webhooks/providers/github.ts | 10 ---- apps/sim/lib/webhooks/providers/intercom.ts | 5 +- apps/sim/lib/webhooks/providers/whatsapp.ts | 2 +- .../executor/human-in-the-loop-manager.ts | 2 +- apps/sim/tools/microsoft_teams/utils.ts | 4 +- 41 files changed, 363 insertions(+), 170 deletions(-) diff --git a/apps/sim/app/api/files/authorization.ts b/apps/sim/app/api/files/authorization.ts index ef5183ae0da..8594380bb6f 100644 --- a/apps/sim/app/api/files/authorization.ts +++ b/apps/sim/app/api/files/authorization.ts @@ -598,7 +598,7 @@ async function authorizeFileAccess( */ export async function assertToolFileAccess( key: unknown, - userId: string | undefined, + userId: string, requestId: string, routeLogger: ReturnType ): Promise { @@ -606,10 +606,6 @@ export async function assertToolFileAccess( routeLogger.warn(`[${requestId}] File access check rejected: missing key`) return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 }) } - if (!userId) { - routeLogger.warn(`[${requestId}] File access check requires userId but none available`) - return NextResponse.json({ success: false, error: 'File not found' }, { status: 404 }) - } const hasAccess = await verifyFileAccess(key, userId) if (!hasAccess) { routeLogger.warn(`[${requestId}] File access denied for user`, { userId, key }) diff --git a/apps/sim/app/api/tools/agiloft/attach/route.ts b/apps/sim/app/api/tools/agiloft/attach/route.ts index edcbdc4c0f3..6257502ae4c 100644 --- a/apps/sim/app/api/tools/agiloft/attach/route.ts +++ b/apps/sim/app/api/tools/agiloft/attach/route.ts @@ -10,6 +10,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { agiloftLogin, agiloftLogout, buildAttachFileUrl } from '@/tools/agiloft/utils' export const dynamic = 'force-dynamic' @@ -22,7 +23,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Agiloft attach attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, @@ -66,6 +67,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { `[${requestId}] Downloading file for Agiloft attach: ${userFile.name} (${userFile.size} bytes)` ) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied const fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) const resolvedFileName = data.fileName || userFile.name || 'attachment' diff --git a/apps/sim/app/api/tools/box/upload/route.ts b/apps/sim/app/api/tools/box/upload/route.ts index 9bd50e77634..73519befcd7 100644 --- a/apps/sim/app/api/tools/box/upload/route.ts +++ b/apps/sim/app/api/tools/box/upload/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Box upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, @@ -49,6 +50,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const userFile = userFiles[0] logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) fileName = validatedData.fileName || userFile.name } else if (validatedData.fileContent) { diff --git a/apps/sim/app/api/tools/confluence/upload-attachment/route.ts b/apps/sim/app/api/tools/confluence/upload-attachment/route.ts index 713ee1a011a..32ae35a0a27 100644 --- a/apps/sim/app/api/tools/confluence/upload-attachment/route.ts +++ b/apps/sim/app/api/tools/confluence/upload-attachment/route.ts @@ -7,6 +7,7 @@ import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { getConfluenceCloudId } from '@/tools/confluence/utils' import { parseAtlassianErrorMessage } from '@/tools/jira/utils' @@ -80,6 +81,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const denied = await assertToolFileAccess( + userFile.key, + auth.userId, + 'confluence-upload', + logger + ) + if (denied) return denied + let fileBuffer: Buffer try { fileBuffer = await downloadFileFromStorage(userFile, 'confluence-upload', logger) diff --git a/apps/sim/app/api/tools/discord/send-message/route.ts b/apps/sim/app/api/tools/discord/send-message/route.ts index 061fec94de7..9a4c24096d4 100644 --- a/apps/sim/app/api/tools/discord/send-message/route.ts +++ b/apps/sim/app/api/tools/discord/send-message/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`) return NextResponse.json( { @@ -30,8 +31,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated Discord send request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(discordSendMessageContract, request, {}) @@ -134,17 +136,30 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } formData.append('payload_json', JSON.stringify(payload)) - const downloadedFiles = await Promise.all( - userFiles.map(async (userFile, i) => { - logger.info(`[${requestId}] Downloading file ${i}: ${userFile.name}`) - const buffer = await downloadFileFromStorage(userFile, requestId, logger) - logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`) - return { userFile, buffer } + const accessResults = await Promise.all( + userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( + userFiles.map(async (file, i) => { + try { + logger.info(`[${requestId}] Downloading file ${i}: ${file.name}`) + return await downloadFileFromStorage(file, requestId, logger) + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } }) ) - for (let i = 0; i < downloadedFiles.length; i++) { - const { userFile, buffer } = downloadedFiles[i] + for (let i = 0; i < userFiles.length; i++) { + const userFile = userFiles[i] + const buffer = buffers[i] + logger.info(`[${requestId}] Added file ${i}: ${userFile.name} (${buffer.length} bytes)`) filesOutput.push({ name: userFile.name, mimeType: userFile.type || 'application/octet-stream', diff --git a/apps/sim/app/api/tools/docusign/route.ts b/apps/sim/app/api/tools/docusign/route.ts index a1352435b6f..04c363def3b 100644 --- a/apps/sim/app/api/tools/docusign/route.ts +++ b/apps/sim/app/api/tools/docusign/route.ts @@ -7,6 +7,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { FileInputSchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' const logger = createLogger('DocuSignAPI') @@ -54,7 +55,7 @@ async function resolveAccount(accessToken: string): Promise export const POST = withRouteHandler(async (request: NextRequest) => { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) } @@ -84,7 +85,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { switch (operation) { case 'send_envelope': - return await handleSendEnvelope(apiBase, headers, params) + return await handleSendEnvelope(apiBase, headers, params, authResult.userId) case 'create_from_template': return await handleCreateFromTemplate(apiBase, headers, params) case 'get_envelope': @@ -115,7 +116,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { async function handleSendEnvelope( apiBase: string, headers: Record, - params: Record + params: Record, + userId: string ) { const { signerEmail, signerName, emailSubject, emailBody, ccEmail, ccName, file, status } = params @@ -135,6 +137,8 @@ async function handleSendEnvelope( const userFiles = processFilesToUserFiles([parsed as RawFileInput], 'docusign-send', logger) if (userFiles.length > 0) { const userFile = userFiles[0] + const denied = await assertToolFileAccess(userFile.key, userId, 'docusign-send', logger) + if (denied) return denied const buffer = await downloadFileFromStorage(userFile, 'docusign-send', logger) documentBase64 = buffer.toString('base64') documentName = userFile.name diff --git a/apps/sim/app/api/tools/dropbox/upload/route.ts b/apps/sim/app/api/tools/dropbox/upload/route.ts index 055ccb140ae..2c14fcd0dc6 100644 --- a/apps/sim/app/api/tools/dropbox/upload/route.ts +++ b/apps/sim/app/api/tools/dropbox/upload/route.ts @@ -8,6 +8,7 @@ import { httpHeaderSafeJson } from '@/lib/core/utils/validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Dropbox upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, @@ -52,6 +53,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const userFile = userFiles[0] logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) fileName = userFile.name } else if (validatedData.fileContent) { diff --git a/apps/sim/app/api/tools/firecrawl/parse/route.ts b/apps/sim/app/api/tools/firecrawl/parse/route.ts index 1c46e85651e..409f74a6f16 100644 --- a/apps/sim/app/api/tools/firecrawl/parse/route.ts +++ b/apps/sim/app/api/tools/firecrawl/parse/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -43,6 +44,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { size: userFile.size, }) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + const buffer = await downloadFileFromStorage(userFile, requestId, logger) const formData = new FormData() diff --git a/apps/sim/app/api/tools/gmail/draft/route.ts b/apps/sim/app/api/tools/gmail/draft/route.ts index bbd5ee5d94d..1b51ff59d28 100644 --- a/apps/sim/app/api/tools/gmail/draft/route.ts +++ b/apps/sim/app/api/tools/gmail/draft/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { base64UrlEncode, buildMimeMessage, @@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`) return NextResponse.json( { @@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated Gmail draft request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(gmailDraftContract, request, {}) @@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentBuffers = await Promise.all( + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( attachments.map(async (file) => { try { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) - - const buffer = await downloadFileFromStorage(file, requestId, logger) - - return { - filename: file.name, - mimeType: file.type || 'application/octet-stream', - content: buffer, - } + return await downloadFileFromStorage(file, requestId, logger) } catch (error) { logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) throw new Error( @@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) ) + const attachmentBuffers = attachments.map((file, i) => ({ + filename: file.name, + mimeType: file.type || 'application/octet-stream', + content: buffers[i], + })) + const mimeMessage = buildMimeMessage({ to: validatedData.to, cc: validatedData.cc ?? undefined, diff --git a/apps/sim/app/api/tools/gmail/edit-draft/route.ts b/apps/sim/app/api/tools/gmail/edit-draft/route.ts index a9515aff73d..dc8b3e71785 100644 --- a/apps/sim/app/api/tools/gmail/edit-draft/route.ts +++ b/apps/sim/app/api/tools/gmail/edit-draft/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { base64UrlEncode, buildMimeMessage, @@ -25,7 +26,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Gmail edit draft attempt: ${authResult.error}`) return NextResponse.json( { @@ -36,9 +37,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info( `[${requestId}] Authenticated Gmail edit draft request via ${authResult.authType}`, - { userId: authResult.userId } + { userId } ) const parsed = await parseRequest(gmailEditDraftContract, request, {}) @@ -81,17 +83,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentBuffers = await Promise.all( + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( attachments.map(async (file) => { - const buffer = await downloadFileFromStorage(file, requestId, logger) - return { - filename: file.name, - mimeType: file.type || 'application/octet-stream', - content: buffer, + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + return await downloadFileFromStorage(file, requestId, logger) + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) } }) ) + const attachmentBuffers = attachments.map((file, i) => ({ + filename: file.name, + mimeType: file.type || 'application/octet-stream', + content: buffers[i], + })) + const mimeMessage = buildMimeMessage({ to: validatedData.to, cc: validatedData.cc ?? undefined, diff --git a/apps/sim/app/api/tools/gmail/send/route.ts b/apps/sim/app/api/tools/gmail/send/route.ts index 028216e6283..d0e6d1b6401 100644 --- a/apps/sim/app/api/tools/gmail/send/route.ts +++ b/apps/sim/app/api/tools/gmail/send/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { base64UrlEncode, buildMimeMessage, @@ -26,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Gmail send attempt: ${authResult.error}`) return NextResponse.json( { @@ -37,8 +38,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated Gmail send request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(gmailSendContract, request, {}) @@ -85,20 +87,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentBuffers = await Promise.all( + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( attachments.map(async (file) => { try { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) - - const buffer = await downloadFileFromStorage(file, requestId, logger) - - return { - filename: file.name, - mimeType: file.type || 'application/octet-stream', - content: buffer, - } + return await downloadFileFromStorage(file, requestId, logger) } catch (error) { logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) throw new Error( @@ -108,6 +109,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) ) + const attachmentBuffers = attachments.map((file, i) => ({ + filename: file.name, + mimeType: file.type || 'application/octet-stream', + content: buffers[i], + })) + const mimeMessage = buildMimeMessage({ to: validatedData.to, cc: validatedData.cc ?? undefined, diff --git a/apps/sim/app/api/tools/google_drive/upload/route.ts b/apps/sim/app/api/tools/google_drive/upload/route.ts index 0222cf03b55..797d7cf11d2 100644 --- a/apps/sim/app/api/tools/google_drive/upload/route.ts +++ b/apps/sim/app/api/tools/google_drive/upload/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { GOOGLE_WORKSPACE_MIME_TYPES, handleSheetsFormat, @@ -52,7 +53,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Google Drive upload attempt: ${authResult.error}`) return NextResponse.json( { @@ -113,6 +114,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { size: userFile.size, }) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + let fileBuffer: Buffer try { diff --git a/apps/sim/app/api/tools/jira/add-attachment/route.ts b/apps/sim/app/api/tools/jira/add-attachment/route.ts index e889ef13745..6f343879d33 100644 --- a/apps/sim/app/api/tools/jira/add-attachment/route.ts +++ b/apps/sim/app/api/tools/jira/add-attachment/route.ts @@ -6,6 +6,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { getJiraCloudId, parseAtlassianErrorMessage } from '@/tools/jira/utils' const logger = createLogger('JiraAddAttachmentAPI') @@ -17,7 +18,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { return NextResponse.json( { success: false, error: authResult.error || 'Unauthorized' }, { status: 401 } @@ -43,6 +44,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const formData = new FormData() for (const file of userFiles) { + const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger) + if (denied) return denied const buffer = await downloadFileFromStorage(file, requestId, logger) const blob = new Blob([new Uint8Array(buffer)], { type: file.type || 'application/octet-stream', diff --git a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts index 31bf0d8fe07..2bfca548e7c 100644 --- a/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts +++ b/apps/sim/app/api/tools/microsoft-dataverse/upload-file/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Dataverse upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, @@ -66,6 +67,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) } else if (validatedData.fileContent) { fileBuffer = Buffer.from(validatedData.fileContent, 'base64') diff --git a/apps/sim/app/api/tools/mistral/parse/route.ts b/apps/sim/app/api/tools/mistral/parse/route.ts index d5334ebf851..c684b9c4492 100644 --- a/apps/sim/app/api/tools/mistral/parse/route.ts +++ b/apps/sim/app/api/tools/mistral/parse/route.ts @@ -14,6 +14,7 @@ import { downloadFileFromStorage, resolveInternalFileUrl, } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -120,6 +121,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } let base64 = userFile.base64 if (!base64) { + const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger) + if (denied) return denied const buffer = await downloadFileFromStorage(userFile, requestId, logger) base64 = buffer.toString('base64') } diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts index 54aafbc0c03..5913cff16e2 100644 --- a/apps/sim/app/api/tools/onedrive/upload/route.ts +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -13,6 +13,7 @@ import { processSingleFileToUserFile, } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { normalizeExcelValues } from '@/tools/onedrive/utils' export const dynamic = 'force-dynamic' @@ -47,7 +48,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized OneDrive upload attempt: ${authResult.error}`) return NextResponse.json( { @@ -108,6 +109,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + try { fileBuffer = await downloadFileFromStorage(userFile, requestId, logger) } catch (error) { diff --git a/apps/sim/app/api/tools/outlook/draft/route.ts b/apps/sim/app/api/tools/outlook/draft/route.ts index 3b2bf4aec1f..65bec69895d 100644 --- a/apps/sim/app/api/tools/outlook/draft/route.ts +++ b/apps/sim/app/api/tools/outlook/draft/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Outlook draft attempt: ${authResult.error}`) return NextResponse.json( { @@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated Outlook draft request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(outlookDraftContract, request, {}) @@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentObjects = await Promise.all( + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( attachments.map(async (file) => { try { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) - - const buffer = await downloadFileFromStorage(file, requestId, logger) - - const base64Content = buffer.toString('base64') - - return { - '@odata.type': '#microsoft.graph.fileAttachment', - name: file.name, - contentType: file.type || 'application/octet-stream', - contentBytes: base64Content, - } + return await downloadFileFromStorage(file, requestId, logger) } catch (error) { logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) throw new Error( @@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) ) + const attachmentObjects = attachments.map((file, i) => ({ + '@odata.type': '#microsoft.graph.fileAttachment', + name: file.name, + contentType: file.type || 'application/octet-stream', + contentBytes: buffers[i].toString('base64'), + })) + logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`) message.attachments = attachmentObjects } diff --git a/apps/sim/app/api/tools/outlook/send/route.ts b/apps/sim/app/api/tools/outlook/send/route.ts index f30f47b13b1..cbd9a175786 100644 --- a/apps/sim/app/api/tools/outlook/send/route.ts +++ b/apps/sim/app/api/tools/outlook/send/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Outlook send attempt: ${authResult.error}`) return NextResponse.json( { @@ -29,8 +30,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated Outlook send request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(outlookSendContract, request, {}) @@ -98,23 +100,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentObjects = await Promise.all( + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( attachments.map(async (file) => { try { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) - - const buffer = await downloadFileFromStorage(file, requestId, logger) - - const base64Content = buffer.toString('base64') - - return { - '@odata.type': '#microsoft.graph.fileAttachment', - name: file.name, - contentType: file.type || 'application/octet-stream', - contentBytes: base64Content, - } + return await downloadFileFromStorage(file, requestId, logger) } catch (error) { logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) throw new Error( @@ -124,6 +122,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) ) + const attachmentObjects = attachments.map((file, i) => ({ + '@odata.type': '#microsoft.graph.fileAttachment', + name: file.name, + contentType: file.type || 'application/octet-stream', + contentBytes: buffers[i].toString('base64'), + })) + logger.info(`[${requestId}] Converted ${attachmentObjects.length} attachments to base64`) message.attachments = attachmentObjects } diff --git a/apps/sim/app/api/tools/quiver/image-to-svg/route.ts b/apps/sim/app/api/tools/quiver/image-to-svg/route.ts index 190c004916b..226e955d848 100644 --- a/apps/sim/app/api/tools/quiver/image-to-svg/route.ts +++ b/apps/sim/app/api/tools/quiver/image-to-svg/route.ts @@ -8,6 +8,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { RawFileInput } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' const logger = createLogger('QuiverImageToSvgAPI') @@ -15,7 +16,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const requestId = generateRequestId() const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 }) } @@ -47,6 +48,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { if (parsed && typeof parsed === 'object') { const userFiles = processFilesToUserFiles([parsed as RawFileInput], requestId, logger) if (userFiles.length > 0) { + const denied = await assertToolFileAccess( + userFiles[0].key, + authResult.userId, + requestId, + logger + ) + if (denied) return denied const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger) apiImage = { base64: buffer.toString('base64') } } else { @@ -64,6 +72,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } else if (typeof data.image === 'object' && data.image !== null) { const userFiles = processFilesToUserFiles([data.image as RawFileInput], requestId, logger) if (userFiles.length > 0) { + const denied = await assertToolFileAccess( + userFiles[0].key, + authResult.userId, + requestId, + logger + ) + if (denied) return denied const buffer = await downloadFileFromStorage(userFiles[0], requestId, logger) apiImage = { base64: buffer.toString('base64') } } else { diff --git a/apps/sim/app/api/tools/s3/put-object/route.ts b/apps/sim/app/api/tools/s3/put-object/route.ts index be4ee9fa81a..a2798c4fc17 100644 --- a/apps/sim/app/api/tools/s3/put-object/route.ts +++ b/apps/sim/app/api/tools/s3/put-object/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized S3 put object attempt: ${authResult.error}`) return NextResponse.json( { @@ -76,6 +77,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + const buffer = await downloadFileFromStorage(userFile, requestId, logger) uploadBody = buffer diff --git a/apps/sim/app/api/tools/sendgrid/send-mail/route.ts b/apps/sim/app/api/tools/sendgrid/send-mail/route.ts index b4a9844bc97..5c4c13952e9 100644 --- a/apps/sim/app/api/tools/sendgrid/send-mail/route.ts +++ b/apps/sim/app/api/tools/sendgrid/send-mail/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -18,7 +19,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SendGrid send attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, @@ -26,6 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated SendGrid send request via ${authResult.authType}`) const parsed = await parseRequest(sendGridSendMailContract, request, {}) @@ -97,20 +99,19 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const userFiles = processFilesToUserFiles(rawAttachments, requestId, logger) if (userFiles.length > 0) { - const sendGridAttachments = await Promise.all( + const accessResults = await Promise.all( + userFiles.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( userFiles.map(async (file) => { try { logger.info( `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` ) - const buffer = await downloadFileFromStorage(file, requestId, logger) - - return { - content: buffer.toString('base64'), - filename: file.name, - type: file.type || 'application/octet-stream', - disposition: 'attachment', - } + return await downloadFileFromStorage(file, requestId, logger) } catch (error) { logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) throw new Error( @@ -120,6 +121,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { }) ) + const sendGridAttachments = userFiles.map((file, i) => ({ + content: buffers[i].toString('base64'), + filename: file.name, + type: file.type || 'application/octet-stream', + disposition: 'attachment', + })) + mailBody.attachments = sendGridAttachments } } diff --git a/apps/sim/app/api/tools/sftp/upload/route.ts b/apps/sim/app/api/tools/sftp/upload/route.ts index 8acf93ca585..b7198ee4368 100644 --- a/apps/sim/app/api/tools/sftp/upload/route.ts +++ b/apps/sim/app/api/tools/sftp/upload/route.ts @@ -27,7 +27,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SFTP upload attempt: ${authResult.error}`) return NextResponse.json( { success: false, error: authResult.error || 'Authentication required' }, diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index 2229d1ecc6a..af975058eb1 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -24,7 +24,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SharePoint upload attempt: ${authResult.error}`) return NextResponse.json( { diff --git a/apps/sim/app/api/tools/smtp/send/route.ts b/apps/sim/app/api/tools/smtp/send/route.ts index ea1f5e16d51..499768b407a 100644 --- a/apps/sim/app/api/tools/smtp/send/route.ts +++ b/apps/sim/app/api/tools/smtp/send/route.ts @@ -22,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized SMTP send attempt: ${authResult.error}`) return NextResponse.json( { @@ -33,8 +33,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const userId = authResult.userId logger.info(`[${requestId}] Authenticated SMTP request via ${authResult.authType}`, { - userId: authResult.userId, + userId, }) const parsed = await parseRequest(smtpSendContract, request, {}) @@ -120,25 +121,33 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } - const attachmentBuffers: { filename: string; content: Buffer; contentType: string }[] = [] - for (const file of attachments) { - const denied = await assertToolFileAccess(file.key, authResult.userId, requestId, logger) - if (denied) return denied - try { - logger.info(`[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)`) - const buffer = await downloadFileFromStorage(file, requestId, logger) - attachmentBuffers.push({ - filename: file.name, - content: buffer, - contentType: file.type || 'application/octet-stream', - }) - } catch (error) { - logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) - throw new Error( - `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` - ) - } - } + const accessResults = await Promise.all( + attachments.map((file) => assertToolFileAccess(file.key, userId, requestId, logger)) + ) + const denied = accessResults.find((r) => r !== null) + if (denied) return denied + + const buffers = await Promise.all( + attachments.map(async (file) => { + try { + logger.info( + `[${requestId}] Downloading attachment: ${file.name} (${file.size} bytes)` + ) + return await downloadFileFromStorage(file, requestId, logger) + } catch (error) { + logger.error(`[${requestId}] Failed to download attachment ${file.name}:`, error) + throw new Error( + `Failed to download attachment "${file.name}": ${error instanceof Error ? error.message : 'Unknown error'}` + ) + } + }) + ) + + const attachmentBuffers = attachments.map((file, i) => ({ + filename: file.name, + content: buffers[i], + contentType: file.type || 'application/octet-stream', + })) logger.info(`[${requestId}] Processed ${attachmentBuffers.length} attachment(s)`) mailOptions.attachments = attachmentBuffers diff --git a/apps/sim/app/api/tools/supabase/storage-upload/route.ts b/apps/sim/app/api/tools/supabase/storage-upload/route.ts index 8e63421e320..3e9a1ee8640 100644 --- a/apps/sim/app/api/tools/supabase/storage-upload/route.ts +++ b/apps/sim/app/api/tools/supabase/storage-upload/route.ts @@ -8,6 +8,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' export const dynamic = 'force-dynamic' @@ -19,7 +20,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn( `[${requestId}] Unauthorized Supabase storage upload attempt: ${authResult.error}` ) @@ -143,6 +144,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => { ) } + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied const buffer = await downloadFileFromStorage(userFile, requestId, logger) uploadBody = buffer diff --git a/apps/sim/app/api/tools/telegram/send-document/route.ts b/apps/sim/app/api/tools/telegram/send-document/route.ts index 9c27e23ae1d..c9c51c7003e 100644 --- a/apps/sim/app/api/tools/telegram/send-document/route.ts +++ b/apps/sim/app/api/tools/telegram/send-document/route.ts @@ -7,6 +7,7 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { convertMarkdownToHTML } from '@/tools/telegram/utils' export const dynamic = 'force-dynamic' @@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { requireWorkflowId: false, }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Telegram send attempt: ${authResult.error}`) return NextResponse.json( { @@ -88,6 +89,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => { const userFile = userFiles[0] logger.info(`[${requestId}] Uploading document: ${userFile.name}`) + const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger) + if (denied) return denied + const buffer = await downloadFileFromStorage(userFile, requestId, logger) const filesOutput = [ { diff --git a/apps/sim/app/api/tools/video/route.ts b/apps/sim/app/api/tools/video/route.ts index ae1021f0fa9..f5f7969ff29 100644 --- a/apps/sim/app/api/tools/video/route.ts +++ b/apps/sim/app/api/tools/video/route.ts @@ -8,6 +8,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import type { UserFile } from '@/executor/types' const logger = createLogger('VideoProxyAPI') @@ -21,7 +22,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } @@ -100,6 +101,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { let jobId: string | undefined let actualDuration: number | undefined + if (body.visualReference) { + const denied = await assertToolFileAccess( + body.visualReference.key, + authResult.userId, + requestId, + logger + ) + if (denied) return denied + } + try { if (provider === 'runway') { const result = await generateWithRunway( diff --git a/apps/sim/app/api/tools/vision/analyze/route.ts b/apps/sim/app/api/tools/vision/analyze/route.ts index 9b8183d413b..4779dcb86fa 100644 --- a/apps/sim/app/api/tools/vision/analyze/route.ts +++ b/apps/sim/app/api/tools/vision/analyze/route.ts @@ -15,6 +15,7 @@ import { downloadFileFromStorage, resolveInternalFileUrl, } from '@/lib/uploads/utils/file-utils.server' +import { assertToolFileAccess } from '@/app/api/files/authorization' import { convertUsageMetadata, extractTextContent } from '@/providers/google/utils' export const dynamic = 'force-dynamic' @@ -27,7 +28,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized Vision analyze attempt: ${authResult.error}`) return NextResponse.json( { @@ -87,6 +88,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => { let base64 = userFile.base64 let bufferLength = 0 if (!base64) { + const denied = await assertToolFileAccess( + userFile.key, + authResult.userId, + requestId, + logger + ) + if (denied) return denied const buffer = await downloadFileFromStorage(userFile, requestId, logger) base64 = buffer.toString('base64') bufferLength = buffer.length diff --git a/apps/sim/app/api/tools/wordpress/upload/route.ts b/apps/sim/app/api/tools/wordpress/upload/route.ts index 859aef52f54..5b9a1cf9383 100644 --- a/apps/sim/app/api/tools/wordpress/upload/route.ts +++ b/apps/sim/app/api/tools/wordpress/upload/route.ts @@ -25,7 +25,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { try { const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) - if (!authResult.success) { + if (!authResult.success || !authResult.userId) { logger.warn(`[${requestId}] Unauthorized WordPress upload attempt: ${authResult.error}`) return NextResponse.json( { diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index 64c364677d7..fc3442c124f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -98,7 +98,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ - - -

{displayName}

-
- + + + + + )} + {showGapAfter && (
)}