Files
pangolin/server/routers/auditLogs/generateCSV.ts
T
Dhananjay Maurya ae36ca3127 Keep plain negative numbers numeric in CSV export
A leading '-' only matters when the value is not a plain number, so
-12 and -1.5 stay numeric while payloads like -2+3+cmd|'/c calc'!A0
still get the apostrophe prefix.
2026-10-13 00:06:15 +05:30

57 lines
2.2 KiB
TypeScript

// Spreadsheet applications (Excel, LibreOffice, Google Sheets) evaluate a cell
// as a formula when it starts with one of these characters, even if the cell is
// quoted. Log exports carry attacker controlled text in every column, so a
// leading apostrophe is prefixed to force the cell to be read as literal text.
const FORMULA_PREFIX = /^[=+\-@\t\r]/;
// A leading "-" alone is not dangerous when the rest of the value is just a
// number: spreadsheets read -12 as a number, not a formula. Only values like
// "-2+3+cmd|'/c calc'!A0" (a DDE payload) actually need neutralising, so
// plain negative numbers stay numeric instead of being forced to text.
const NEGATIVE_NUMBER = /^-\d+(\.\d+)?$/;
// CRLF, CR and LF each terminate a record for at least one CSV parser, so a
// value containing them would otherwise spill across rows and forge new ones.
// Replacing them with a literal "\n" keeps every record on a single line.
const LINE_BREAKS = /\r\n|\r|\n/g;
/**
* Render a single value as a CSV field that cannot be reinterpreted by a
* spreadsheet application or reshape the surrounding file.
*/
function toCsvField(value: unknown): string {
// Array#join renders null and undefined as an empty field, which is what the
// export has always emitted for those columns, so keep that behaviour.
if (value === null || value === undefined) {
return "";
}
const text = String(value).replace(LINE_BREAKS, "\\n");
// Quoting does not stop a spreadsheet from evaluating the unquoted value,
// so the apostrophe has to be added before the field is escaped.
const cell =
FORMULA_PREFIX.test(text) && !NEGATIVE_NUMBER.test(text)
? `'${text}`
: text;
if (cell.includes(",") || cell.includes('"')) {
return `"${cell.replace(/"/g, '""')}"`;
}
return cell;
}
export function generateCSV(data: any[]): string {
if (data.length === 0) {
return "orgId,action,actorType,timestamp,actor\n";
}
const headers = Object.keys(data[0]).map(toCsvField).join(",");
const rows = data.map((row) =>
Object.values(row).map(toCsvField).join(",")
);
return [headers, ...rows].join("\n");
}