From 5d37621c09d16f081ea35afbbd97226fbbfe4446 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 10 Jun 2026 13:36:43 -0700 Subject: [PATCH 1/3] fix(security): neutralize CSV formula injection in logs export --- apps/sim/app/api/logs/export/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 766435eadd4..f0787401e43 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -14,9 +14,17 @@ const logger = createLogger('LogsExportAPI') export const revalidate = 0 +/** + * Prefixes a single quote to values starting with a spreadsheet formula trigger + * (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets. + */ +function neutralizeCsvFormula(value: string): string { + return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value +} + function escapeCsv(value: any): string { if (value === null || value === undefined) return '' - const str = String(value) + const str = neutralizeCsvFormula(String(value)) if (/[",\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"` } From 2e0de9091aa2bce5d5c033fedef8cad9805be8cb Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 10 Jun 2026 13:43:44 -0700 Subject: [PATCH 2/3] refactor(csv): extract neutralizeCsvFormula to shared util --- apps/sim/app/api/logs/export/route.ts | 9 +-------- apps/sim/app/api/table/[tableId]/export/route.ts | 9 +-------- apps/sim/lib/core/utils/csv.ts | 7 +++++++ 3 files changed, 9 insertions(+), 16 deletions(-) create mode 100644 apps/sim/lib/core/utils/csv.ts diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index f0787401e43..55c032cf8e4 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -5,6 +5,7 @@ import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' +import { neutralizeCsvFormula } from '@/lib/core/utils/csv' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' @@ -14,14 +15,6 @@ const logger = createLogger('LogsExportAPI') export const revalidate = 0 -/** - * Prefixes a single quote to values starting with a spreadsheet formula trigger - * (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets. - */ -function neutralizeCsvFormula(value: string): string { - return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value -} - function escapeCsv(value: any): string { if (value === null || value === undefined) return '' const str = neutralizeCsvFormula(String(value)) diff --git a/apps/sim/app/api/table/[tableId]/export/route.ts b/apps/sim/app/api/table/[tableId]/export/route.ts index b1fbef15aec..096f9d6c5af 100644 --- a/apps/sim/app/api/table/[tableId]/export/route.ts +++ b/apps/sim/app/api/table/[tableId]/export/route.ts @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { tableExportFormatSchema, tableIdParamsSchema } from '@/lib/api/contracts/tables' import { getValidationErrorMessage } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { neutralizeCsvFormula } from '@/lib/core/utils/csv' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildNameById, getColumnId, rowDataIdToName } from '@/lib/table/column-keys' @@ -119,14 +120,6 @@ function sanitizeFilename(name: string): string { return cleaned || 'table' } -/** - * Prefixes a single quote to values starting with a spreadsheet formula trigger - * (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets. - */ -function neutralizeCsvFormula(value: string): string { - return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value -} - /** * Serializes a cell for CSV. Only string cells are formula-neutralized; numbers, * booleans, dates, and JSON objects can never form a trigger and pass through verbatim. diff --git a/apps/sim/lib/core/utils/csv.ts b/apps/sim/lib/core/utils/csv.ts new file mode 100644 index 00000000000..c02bf5a2314 --- /dev/null +++ b/apps/sim/lib/core/utils/csv.ts @@ -0,0 +1,7 @@ +/** + * Prefixes a single quote to values starting with a spreadsheet formula trigger + * (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets. + */ +export function neutralizeCsvFormula(value: string): string { + return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value +} From 8e8bb1292eb941ea1847de2bbdcb93e5783bef05 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 10 Jun 2026 13:49:28 -0700 Subject: [PATCH 3/3] fix(csv): only neutralize string cells in logs export --- apps/sim/app/api/logs/export/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index 55c032cf8e4..f85816e26a1 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -17,7 +17,7 @@ export const revalidate = 0 function escapeCsv(value: any): string { if (value === null || value === undefined) return '' - const str = neutralizeCsvFormula(String(value)) + const str = typeof value === 'string' ? neutralizeCsvFormula(value) : String(value) if (/[",\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"` }