diff --git a/apps/sim/app/api/help/route.ts b/apps/sim/app/api/help/route.ts index 6fe478fe4b4..3c10f68f4d4 100644 --- a/apps/sim/app/api/help/route.ts +++ b/apps/sim/app/api/help/route.ts @@ -5,12 +5,26 @@ import { helpFormBodySchema } from '@/lib/api/contracts/common' import { validationErrorResponse } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' +import { + isPayloadSizeLimitError, + MAX_MULTIPART_OVERHEAD_BYTES, + readFormDataWithLimit, +} from '@/lib/core/utils/stream-limits' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress, getHelpEmailAddress } from '@/lib/messaging/email/utils' +import { MAX_WORKSPACE_FORMDATA_FILE_SIZE } from '@/lib/uploads/shared/types' const logger = createLogger('HelpAPI') +/** + * The form can carry several image attachments with no server-side count + * cap, so this reuses the repo's largest existing per-request form-data + * bound (see files/upload route) rather than an arbitrary smaller limit + * that could reject a legitimate multi-image submission. + */ +const MAX_HELP_FORM_BYTES = MAX_WORKSPACE_FORMDATA_FILE_SIZE + MAX_MULTIPART_OVERHEAD_BYTES + export const POST = withRouteHandler(async (req: NextRequest) => { const requestId = generateRequestId() @@ -23,7 +37,10 @@ export const POST = withRouteHandler(async (req: NextRequest) => { const email = session.user.email - const formData = await req.formData() + const formData = await readFormDataWithLimit(req, { + maxBytes: MAX_HELP_FORM_BYTES, + label: 'Help request form data', + }) const subject = formData.get('subject') as string const message = formData.get('message') as string @@ -128,6 +145,13 @@ ${message} { status: 200 } ) } catch (error) { + if (isPayloadSizeLimitError(error)) { + logger.warn(`[${requestId}] Help request form data too large`, { message: error.message }) + return NextResponse.json( + { error: `Request body exceeds the maximum allowed size of ${MAX_HELP_FORM_BYTES} bytes` }, + { status: 413 } + ) + } if (error instanceof Error && error.message.includes('not configured')) { logger.error(`[${requestId}] Email service configuration error`, error) return NextResponse.json( diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts index 059abee2fb6..75d9df48173 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts @@ -7,7 +7,7 @@ import { createChunkBodySchema, listKnowledgeChunksQuerySchema, } from '@/lib/api/contracts/knowledge' -import { isZodError, parseRequest } from '@/lib/api/server' +import { isZodError, parseJsonBody, parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -103,9 +103,6 @@ export const POST = withRouteHandler( const { id: knowledgeBaseId, documentId } = await params try { - const body = await req.json() - const { workflowId, ...searchParams } = body - const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Authentication failed: ${auth.error || 'Unauthorized'}`) @@ -113,7 +110,14 @@ export const POST = withRouteHandler( } const userId = auth.userId + const parsedBody = await parseJsonBody(req) + if (!parsedBody.success) return parsedBody.response + const { workflowId, ...searchParams } = parsedBody.data as Record + if (workflowId) { + if (typeof workflowId !== 'string') { + return NextResponse.json({ error: 'workflowId must be a string' }, { status: 400 }) + } const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, diff --git a/apps/sim/app/api/speech/token/route.test.ts b/apps/sim/app/api/speech/token/route.test.ts index 8c00a1c7787..a555948c9d9 100644 --- a/apps/sim/app/api/speech/token/route.test.ts +++ b/apps/sim/app/api/speech/token/route.test.ts @@ -138,4 +138,13 @@ describe('POST /api/speech/token — usage attribution', () => { workspaceId: 'ws-1', }) }) + + it('rejects an oversized body before any auth/billing work runs', async () => { + const oversizedBody = { chatId: 'x'.repeat(64 * 1024) } + const res = await POST(createMockRequest('POST', oversizedBody)) + + expect(res.status).toBe(413) + expect(mockGetSession).not.toHaveBeenCalled() + expect(mockRecordUsage).not.toHaveBeenCalled() + }) }) diff --git a/apps/sim/app/api/speech/token/route.ts b/apps/sim/app/api/speech/token/route.ts index 3ccb721366f..47a63b741dc 100644 --- a/apps/sim/app/api/speech/token/route.ts +++ b/apps/sim/app/api/speech/token/route.ts @@ -6,6 +6,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { speechTokenBodySchema } from '@/lib/api/contracts/media/speech' +import { parseOptionalJsonBody } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor' import { recordUsage } from '@/lib/billing/core/usage-log' @@ -34,6 +35,13 @@ const STT_TOKEN_RATE_LIMIT = { refillIntervalMs: 72 * 1000, } as const +/** + * This body only ever carries an optional chatId/workspaceId string, so a + * tight cap keeps an unauthenticated caller from forcing a large in-memory + * allocation before the auth checks below run. + */ +const MAX_SPEECH_TOKEN_BODY_BYTES = 16 * 1024 + function hashVoiceToken(token: string): string { return createHash('sha256').update(token).digest('hex') } @@ -87,8 +95,9 @@ async function validateChatAuth( export const POST = withRouteHandler(async (request: NextRequest) => { try { - const rawBody = await request.json().catch(() => ({})) - const body = speechTokenBodySchema.safeParse(rawBody) + const parsedBody = await parseOptionalJsonBody(request, MAX_SPEECH_TOKEN_BODY_BYTES) + if (!parsedBody.success) return parsedBody.response + const body = speechTokenBodySchema.safeParse(parsedBody.data ?? {}) const chatId = body.success && typeof body.data.chatId === 'string' ? body.data.chatId : undefined