diff --git a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts index 5e0f358ea..b6c0bbd0a 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_channel/route.ts @@ -2,10 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { - secureFetchWithPinnedIP, - validateUrlWithDNS, -} from '@/lib/core/security/input-validation.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -29,22 +26,6 @@ const TeamsWriteChannelSchema = z.object({ files: RawFileInputArraySchema.optional().nullable(), }) -async function secureFetchGraph( - url: string, - options: { - method?: string - headers?: Record - body?: string | Buffer | Uint8Array - }, - paramName: string -) { - const urlValidation = await validateUrlWithDNS(url, paramName) - if (!urlValidation.isValid) { - throw new Error(urlValidation.error) - } - return secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, options) -} - export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -123,7 +104,7 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) - const uploadResponse = await secureFetchGraph( + const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', @@ -154,7 +135,7 @@ export async function POST(request: NextRequest) { const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?$select=id,name,webDavUrl,eTag,size` - const fileDetailsResponse = await secureFetchGraph( + const fileDetailsResponse = await secureFetchWithValidation( fileDetailsUrl, { method: 'GET', @@ -260,7 +241,7 @@ export async function POST(request: NextRequest) { const teamsUrl = `https://graph.microsoft.com/v1.0/teams/${encodeURIComponent(validatedData.teamId)}/channels/${encodeURIComponent(validatedData.channelId)}/messages` - const teamsResponse = await secureFetchGraph( + const teamsResponse = await secureFetchWithValidation( teamsUrl, { method: 'POST', diff --git a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts index 6a4e929ba..ec8d43d8a 100644 --- a/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts +++ b/apps/sim/app/api/tools/microsoft_teams/write_chat/route.ts @@ -2,10 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { - secureFetchWithPinnedIP, - validateUrlWithDNS, -} from '@/lib/core/security/input-validation.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -28,22 +25,6 @@ const TeamsWriteChatSchema = z.object({ files: RawFileInputArraySchema.optional().nullable(), }) -async function secureFetchGraph( - url: string, - options: { - method?: string - headers?: Record - body?: string | Buffer | Uint8Array - }, - paramName: string -) { - const urlValidation = await validateUrlWithDNS(url, paramName) - if (!urlValidation.isValid) { - throw new Error(urlValidation.error) - } - return secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, options) -} - export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -92,6 +73,18 @@ export async function POST(request: NextRequest) { for (const file of userFiles) { try { + // Microsoft Graph API limits direct uploads to 4MB + const maxSize = 4 * 1024 * 1024 + if (file.size > maxSize) { + const sizeMB = (file.size / (1024 * 1024)).toFixed(2) + logger.error( + `[${requestId}] File ${file.name} is ${sizeMB}MB, exceeds 4MB limit for direct upload` + ) + throw new Error( + `File "${file.name}" (${sizeMB}MB) exceeds the 4MB limit for Teams attachments. Use smaller files or upload to SharePoint/OneDrive first.` + ) + } + logger.info(`[${requestId}] Uploading file to Teams: ${file.name} (${file.size} bytes)`) const buffer = await downloadFileFromStorage(file, requestId, logger) @@ -109,7 +102,7 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading to Teams: ${uploadUrl}`) - const uploadResponse = await secureFetchGraph( + const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', @@ -140,7 +133,7 @@ export async function POST(request: NextRequest) { const fileDetailsUrl = `https://graph.microsoft.com/v1.0/me/drive/items/${uploadedFile.id}?$select=id,name,webDavUrl,eTag,size` - const fileDetailsResponse = await secureFetchGraph( + const fileDetailsResponse = await secureFetchWithValidation( fileDetailsUrl, { method: 'GET', @@ -245,7 +238,7 @@ export async function POST(request: NextRequest) { const teamsUrl = `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(validatedData.chatId)}/messages` - const teamsResponse = await secureFetchGraph( + const teamsResponse = await secureFetchWithValidation( teamsUrl, { method: 'POST', diff --git a/apps/sim/app/api/tools/onedrive/upload/route.ts b/apps/sim/app/api/tools/onedrive/upload/route.ts index 691812bc7..63d50ae73 100644 --- a/apps/sim/app/api/tools/onedrive/upload/route.ts +++ b/apps/sim/app/api/tools/onedrive/upload/route.ts @@ -3,10 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import * as XLSX from 'xlsx' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { - secureFetchWithPinnedIP, - validateUrlWithDNS, -} from '@/lib/core/security/input-validation.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputSchema } from '@/lib/uploads/utils/file-schemas' import { @@ -82,22 +79,6 @@ function validateMicrosoftGraphId( return { isValid: true } } -async function secureFetchGraph( - url: string, - options: { - method?: string - headers?: Record - body?: string | Buffer | Uint8Array - }, - paramName: string -) { - const urlValidation = await validateUrlWithDNS(url, paramName) - if (!urlValidation.isValid) { - throw new Error(urlValidation.error) - } - return secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, options) -} - export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -231,7 +212,7 @@ export async function POST(request: NextRequest) { uploadUrl += `?@microsoft.graph.conflictBehavior=${validatedData.conflictBehavior}` } - const uploadResponse = await secureFetchGraph( + const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', @@ -268,7 +249,7 @@ export async function POST(request: NextRequest) { const sessionUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent( fileData.id )}/workbook/createSession` - const sessionResp = await secureFetchGraph( + const sessionResp = await secureFetchWithValidation( sessionUrl, { method: 'POST', @@ -291,7 +272,7 @@ export async function POST(request: NextRequest) { const listUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent( fileData.id )}/workbook/worksheets?$select=name&$orderby=position&$top=1` - const listResp = await secureFetchGraph( + const listResp = await secureFetchWithValidation( listUrl, { method: 'GET', @@ -362,7 +343,7 @@ export async function POST(request: NextRequest) { )}')/range(address='${encodeURIComponent(computedRangeAddress)}')` ) - const excelWriteResponse = await secureFetchGraph( + const excelWriteResponse = await secureFetchWithValidation( url.toString(), { method: 'PATCH', @@ -406,7 +387,7 @@ export async function POST(request: NextRequest) { const closeUrl = `${MICROSOFT_GRAPH_BASE}/me/drive/items/${encodeURIComponent( fileData.id )}/workbook/closeSession` - const closeResp = await secureFetchGraph( + const closeResp = await secureFetchWithValidation( closeUrl, { method: 'POST', diff --git a/apps/sim/app/api/tools/sharepoint/upload/route.ts b/apps/sim/app/api/tools/sharepoint/upload/route.ts index 05392f0bf..4f8f37e12 100644 --- a/apps/sim/app/api/tools/sharepoint/upload/route.ts +++ b/apps/sim/app/api/tools/sharepoint/upload/route.ts @@ -2,10 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkInternalAuth } from '@/lib/auth/hybrid' -import { - secureFetchWithPinnedIP, - validateUrlWithDNS, -} from '@/lib/core/security/input-validation.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { generateRequestId } from '@/lib/core/utils/request' import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' @@ -24,22 +21,6 @@ const SharepointUploadSchema = z.object({ files: RawFileInputArraySchema.optional().nullable(), }) -async function secureFetchGraph( - url: string, - options: { - method?: string - headers?: Record - body?: string | Buffer | Uint8Array - }, - paramName: string -) { - const urlValidation = await validateUrlWithDNS(url, paramName) - if (!urlValidation.isValid) { - throw new Error(urlValidation.error) - } - return secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, options) -} - export async function POST(request: NextRequest) { const requestId = generateRequestId() @@ -101,7 +82,7 @@ export async function POST(request: NextRequest) { if (!effectiveDriveId) { logger.info(`[${requestId}] No driveId provided, fetching default drive for site`) const driveUrl = `https://graph.microsoft.com/v1.0/sites/${validatedData.siteId}/drive` - const driveResponse = await secureFetchGraph( + const driveResponse = await secureFetchWithValidation( driveUrl, { method: 'GET', @@ -171,7 +152,7 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Uploading to: ${uploadUrl}`) - const uploadResponse = await secureFetchGraph( + const uploadResponse = await secureFetchWithValidation( uploadUrl, { method: 'PUT', @@ -192,7 +173,7 @@ export async function POST(request: NextRequest) { // File exists - retry with conflict behavior set to replace logger.warn(`[${requestId}] File ${fileName} already exists, retrying with replace`) const replaceUrl = `${uploadUrl}?@microsoft.graph.conflictBehavior=replace` - const replaceResponse = await secureFetchGraph( + const replaceResponse = await secureFetchWithValidation( replaceUrl, { method: 'PUT', diff --git a/apps/sim/app/api/tools/slack/utils.ts b/apps/sim/app/api/tools/slack/utils.ts index 4577d4491..a14ae74a8 100644 --- a/apps/sim/app/api/tools/slack/utils.ts +++ b/apps/sim/app/api/tools/slack/utils.ts @@ -1,28 +1,9 @@ import type { Logger } from '@sim/logger' -import { - secureFetchWithPinnedIP, - validateUrlWithDNS, -} from '@/lib/core/security/input-validation.server' +import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import type { ToolFileData } from '@/tools/types' -async function secureFetchExternal( - url: string, - options: { - method?: string - headers?: Record - body?: string | Buffer | Uint8Array - }, - paramName: string -) { - const urlValidation = await validateUrlWithDNS(url, paramName) - if (!urlValidation.isValid) { - throw new Error(urlValidation.error) - } - return secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, options) -} - /** * Sends a message to a Slack channel using chat.postMessage */ @@ -128,7 +109,7 @@ export async function uploadFilesToSlack( logger.info(`[${requestId}] Got upload URL for ${userFile.name}, file_id: ${urlData.file_id}`) - const uploadResponse = await secureFetchExternal( + const uploadResponse = await secureFetchWithValidation( urlData.upload_url, { method: 'POST', diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index a9a46b6d2..e8c0ec861 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -288,3 +288,25 @@ export async function secureFetchWithPinnedIP( req.end() }) } + +/** + * Validates a URL and performs a secure fetch with DNS pinning in one call. + * Combines validateUrlWithDNS and secureFetchWithPinnedIP for convenience. + * + * @param url - The URL to fetch + * @param options - Fetch options (method, headers, body, etc.) + * @param paramName - Name of the parameter for error messages (default: 'url') + * @returns SecureFetchResponse + * @throws Error if URL validation fails + */ +export async function secureFetchWithValidation( + url: string, + options: SecureFetchOptions = {}, + paramName = 'url' +): Promise { + const validation = await validateUrlWithDNS(url, paramName) + if (!validation.isValid) { + throw new Error(validation.error) + } + return secureFetchWithPinnedIP(url, validation.resolvedIP!, options) +}