fix: sanitize CSV exports against formula injection (#103)

This commit is contained in:
Ben Senescu 2026-04-08 14:18:27 -04:00 committed by GitHub
parent c9d212b8b8
commit 0b7811d882
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 55 additions and 1 deletions

View File

@ -90,4 +90,39 @@ describe("buildBacklinksTabCsvFile", () => {
); );
expect(file.content).toContain('"https://docs.example.com/start"'); expect(file.content).toContain('"https://docs.example.com/start"');
}); });
it("sanitizes formula-like cell values to prevent CSV injection", () => {
const file = buildBacklinksTabCsvFile({
tab: "backlinks",
target: "example.com",
rows: {
backlinks: [
{
domainFrom: "=cmd|' /C calc'!A0",
urlFrom: "+https://evil.example/source",
urlTo: "@https://evil.example/target",
anchor: "\tformula",
itemType: "organic",
isDofollow: true,
relAttributes: [],
rank: 1,
domainFromRank: 1,
pageFromRank: 1,
spamScore: 0,
firstSeen: "2025-01-01",
lastSeen: "2025-01-01",
isLost: false,
isBroken: false,
linksCount: 1,
},
],
referringDomains: [],
topPages: [],
},
});
expect(file.content).toContain("\"'=cmd|' /C calc'!A0\"");
expect(file.content).toContain('"\'+https://evil.example/source"');
expect(file.content).toContain('"\'@https://evil.example/target"');
expect(file.content).toContain('"\'\tformula"');
});
}); });

View File

@ -3,7 +3,9 @@ import Papa from "papaparse";
type CsvValue = string | number | boolean | null | undefined; type CsvValue = string | number | boolean | null | undefined;
export function buildCsv(headers: string[], rows: CsvValue[][]): string { export function buildCsv(headers: string[], rows: CsvValue[][]): string {
const normalizedRows = rows.map((row) => row.map((value) => value ?? "")); const normalizedRows = rows.map((row) =>
row.map((value) => sanitizeCsvValue(value ?? "")),
);
return Papa.unparse( return Papa.unparse(
{ {
@ -17,6 +19,23 @@ export function buildCsv(headers: string[], rows: CsvValue[][]): string {
); );
} }
// Prevent CSV injection (formula injection) by prefixing dangerous characters
// with a single quote. See OWASP guidance:
// https://owasp.org/www-community/attacks/CSV_Injection
function sanitizeCsvValue(
value: string | number | boolean,
): string | number | boolean {
if (typeof value !== "string" || value.length === 0) {
return value;
}
if (["=", "+", "-", "@", "\t", "\r", "\n"].includes(value[0])) {
return `'${value}`;
}
return value;
}
export function downloadCsv(filename: string, content: string): void { export function downloadCsv(filename: string, content: string): void {
const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); const blob = new Blob([content], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);