mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-19 03:04:39 -05:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e0d003dd6 |
@@ -38,6 +38,38 @@ describe("convertToCsv", () => {
|
||||
|
||||
parseSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should defang formula injection payloads in cell values", async () => {
|
||||
const payloads = [
|
||||
'=HYPERLINK("https://evil.tld","Click")',
|
||||
"+1+1",
|
||||
"-2+3",
|
||||
"@SUM(A1:A2)",
|
||||
"\tleading-tab",
|
||||
"\rleading-cr",
|
||||
];
|
||||
const rows = payloads.map((p) => ({ name: p, age: 0 }));
|
||||
const csv = await convertToCsv(["name", "age"], rows);
|
||||
const lines = csv.trim().split("\n").slice(1); // drop header
|
||||
payloads.forEach((p, i) => {
|
||||
// each value should be prefixed with a single quote so the spreadsheet
|
||||
// app treats it as text rather than a formula
|
||||
expect(lines[i].startsWith(`"'${p.charAt(0)}`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test("should defang formula injection in field/header names", async () => {
|
||||
const csv = await convertToCsv(["=evil", "age"], [{ "=evil": "x", age: 1 }]);
|
||||
const lines = csv.trim().split("\n");
|
||||
expect(lines[0]).toBe('"\'=evil","age"');
|
||||
expect(lines[1]).toBe('"x",1');
|
||||
});
|
||||
|
||||
test("should not alter benign strings", async () => {
|
||||
const csv = await convertToCsv(["name"], [{ name: "Alice = Bob" }]);
|
||||
const lines = csv.trim().split("\n");
|
||||
expect(lines[1]).toBe('"Alice = Bob"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertToXlsxBuffer", () => {
|
||||
@@ -60,4 +92,15 @@ describe("convertToXlsxBuffer", () => {
|
||||
const cleaned = raw.map(({ __rowNum__, ...rest }) => rest);
|
||||
expect(cleaned).toEqual(data);
|
||||
});
|
||||
|
||||
test("should defang formula injection payloads in xlsx cells", () => {
|
||||
const payload = '=HYPERLINK("https://evil.tld","Click")';
|
||||
const buffer = convertToXlsxBuffer(["name"], [{ name: payload }]);
|
||||
const wb = xlsx.read(buffer, { type: "buffer" });
|
||||
const sheet = wb.Sheets["Sheet1"];
|
||||
const cell = sheet["A2"];
|
||||
// value stored as plain text, not as a formula (no `f` property)
|
||||
expect(cell.f).toBeUndefined();
|
||||
expect(cell.v).toBe(`'${payload}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,15 +2,36 @@ import { AsyncParser } from "@json2csv/node";
|
||||
import * as xlsx from "xlsx";
|
||||
import { logger } from "@formbricks/logger";
|
||||
|
||||
// Defang spreadsheet formula injection. Cell values starting with
|
||||
// =, +, -, @, tab, or CR are evaluated as formulas by Excel/Sheets/Numbers.
|
||||
const FORMULA_TRIGGER = /^[=+\-@\t\r]/;
|
||||
|
||||
const sanitizeFormulaInjection = <T>(value: T): T => {
|
||||
if (typeof value === "string" && FORMULA_TRIGGER.test(value)) {
|
||||
return `'${value}` as T;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const sanitizeRows = (rows: Record<string, string | number>[]): Record<string, string | number>[] =>
|
||||
rows.map((row) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(row).map(([key, value]) => [
|
||||
sanitizeFormulaInjection(key),
|
||||
sanitizeFormulaInjection(value),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
export const convertToCsv = async (fields: string[], jsonData: Record<string, string | number>[]) => {
|
||||
let csv: string = "";
|
||||
|
||||
const parser = new AsyncParser({
|
||||
fields,
|
||||
fields: fields.map(sanitizeFormulaInjection),
|
||||
});
|
||||
|
||||
try {
|
||||
csv = await parser.parse(jsonData).promise();
|
||||
csv = await parser.parse(sanitizeRows(jsonData)).promise();
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to convert to CSV");
|
||||
throw new Error("Failed to convert to CSV");
|
||||
@@ -24,7 +45,9 @@ export const convertToXlsxBuffer = (
|
||||
jsonData: Record<string, string | number>[]
|
||||
): Buffer => {
|
||||
const wb = xlsx.utils.book_new();
|
||||
const ws = xlsx.utils.json_to_sheet(jsonData, { header: fields });
|
||||
const ws = xlsx.utils.json_to_sheet(sanitizeRows(jsonData), {
|
||||
header: fields.map(sanitizeFormulaInjection),
|
||||
});
|
||||
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
|
||||
return xlsx.write(wb, { type: "buffer", bookType: "xlsx" });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user