fix: sanitize CSV exports against formula injection (#103)
This commit is contained in:
parent
c9d212b8b8
commit
0b7811d882
@ -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"');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user