Skip to content

Commit dd3705e

Browse files
authored
fix(security): neutralize CSV formula injection in logs export (#4952)
* fix(security): neutralize CSV formula injection in logs export * refactor(csv): extract neutralizeCsvFormula to shared util * fix(csv): only neutralize string cells in logs export
1 parent 20dd654 commit dd3705e

3 files changed

Lines changed: 10 additions & 9 deletions

File tree

apps/sim/app/api/logs/export/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { and, desc, eq, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77
import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
8+
import { neutralizeCsvFormula } from '@/lib/core/utils/csv'
89
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
910
import { materializeExecutionData } from '@/lib/logs/execution/trace-store'
1011
import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters'
@@ -16,7 +17,7 @@ export const revalidate = 0
1617

1718
function escapeCsv(value: any): string {
1819
if (value === null || value === undefined) return ''
19-
const str = String(value)
20+
const str = typeof value === 'string' ? neutralizeCsvFormula(value) : String(value)
2021
if (/[",\n]/.test(str)) {
2122
return `"${str.replace(/"/g, '""')}"`
2223
}

apps/sim/app/api/table/[tableId]/export/route.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { tableExportFormatSchema, tableIdParamsSchema } from '@/lib/api/contracts/tables'
44
import { getValidationErrorMessage } from '@/lib/api/server'
55
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
6+
import { neutralizeCsvFormula } from '@/lib/core/utils/csv'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
89
import { buildNameById, getColumnId, rowDataIdToName } from '@/lib/table/column-keys'
@@ -119,14 +120,6 @@ function sanitizeFilename(name: string): string {
119120
return cleaned || 'table'
120121
}
121122

122-
/**
123-
* Prefixes a single quote to values starting with a spreadsheet formula trigger
124-
* (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets.
125-
*/
126-
function neutralizeCsvFormula(value: string): string {
127-
return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value
128-
}
129-
130123
/**
131124
* Serializes a cell for CSV. Only string cells are formula-neutralized; numbers,
132125
* booleans, dates, and JSON objects can never form a trigger and pass through verbatim.

apps/sim/lib/core/utils/csv.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Prefixes a single quote to values starting with a spreadsheet formula trigger
3+
* (`=`, `+`, `-`, `@`, tab, CR), neutralizing CSV injection in Excel/Sheets.
4+
*/
5+
export function neutralizeCsvFormula(value: string): string {
6+
return /^[=+\-@\t\r]/.test(value) ? `'${value}` : value
7+
}

0 commit comments

Comments
 (0)