Neutralise formula prefixes in CSV log exports

generateCSV escaped quotes only when a value happened to contain a comma and never neutralised formula prefixes, so every CSV log export could emit attacker controlled cells that execute when an admin opens the file. actor is a virtual API key name validated as nothing more than a non-empty string, and path, host, originalRequestURL, metadata and the AI session bodies come straight from the client request.

Each value now goes through a single encoder that collapses CR, LF and CRLF to a literal \n so a record can never be split across rows, prefixes a leading apostrophe when the first character would make a spreadsheet evaluate the cell as a formula, and quotes whenever the field contains a comma or a quote instead of only when it contains a comma. Headers use the same encoder so a column name cannot desynchronise the rows from the header row.

The five CSV export endpoints also declare charset=utf-8, matching the content type already used by the log streaming destination.

Fixes #3864
This commit is contained in:
Dhananjay Maurya
2026-10-08 14:43:43 +05:30
parent 58a3701fa8
commit bae174766b
7 changed files with 234 additions and 13 deletions
@@ -99,7 +99,7 @@ export async function exportAccessAuditLogs(
const csvData = generateCSV(log);
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader(
"Content-Disposition",
`attachment; filename="access-audit-logs-${data.orgId}-${Date.now()}.csv"`
@@ -99,7 +99,7 @@ export async function exportActionAuditLogs(
const csvData = generateCSV(log);
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader(
"Content-Disposition",
`attachment; filename="action-audit-logs-${data.orgId}-${Date.now()}.csv"`
@@ -101,7 +101,7 @@ export async function exportConnectionAuditLogs(
const csvData = generateCSV(log);
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader(
"Content-Disposition",
`attachment; filename="connection-audit-logs-${data.orgId}-${Date.now()}.csv"`