From 9837d386e834c75d777afb0a78758c852ae1c3f9 Mon Sep 17 00:00:00 2001 From: wangtao <328929485+wangtaotaotao95@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:25:07 +0800 Subject: [PATCH] fix: reject malformed quoted transaction fields --- apps/server/src/engine/finance.ts | 14 +++++++++++++- tests/finance.test.ts | 32 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 tests/finance.test.ts diff --git a/apps/server/src/engine/finance.ts b/apps/server/src/engine/finance.ts index c614ebcc4..652e1532d 100644 --- a/apps/server/src/engine/finance.ts +++ b/apps/server/src/engine/finance.ts @@ -14,7 +14,19 @@ export function analyzeSpending(csv: string) { cell += '"'; i++; } else if (!quoted && cell.length) throw new Error("Invalid quoted CSV field"); - else quoted = !quoted; + else { + quoted = !quoted; + if (!quoted) { + const next = csv[i + 1]; + if ( + next !== undefined && + next !== "," && + next !== "\n" && + !(next === "\r" && (csv[i + 2] === "\n" || csv[i + 2] === undefined)) + ) + throw new Error("Invalid quoted CSV field"); + } + } } else if (!quoted && (c === "," || c === "\n" || c === undefined)) { row.push(cell.replace(/\r$/, "")); cell = ""; diff --git a/tests/finance.test.ts b/tests/finance.test.ts new file mode 100644 index 000000000..94ae732d8 --- /dev/null +++ b/tests/finance.test.ts @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { analyzeSpending } from "../apps/server/src/engine/finance.ts"; + +test("transaction imports reject characters after a closing CSV quote", () => { + for (const amount of ['"10"0', '""10', '"10".00', '"10" ']) { + assert.throws( + () => analyzeSpending(`date,description,amount,category\n2026-09-01,Coffee,${amount},Food`), + /Invalid quoted CSV field/, + `must not silently interpret ${amount} as a transaction amount`, + ); + } + assert.throws( + () => analyzeSpending('date,description,amount,category\n2026-09-01,"Coffee"shop,10,Food'), + /Invalid quoted CSV field/, + ); +}); + +test("transaction imports preserve valid quoted fields at CSV boundaries", () => { + for (const ending of ["", "\n", "\r\n", "\r"]) { + const report = analyzeSpending( + '"date","description","amount","category"\r\n' + + '"2026-09-01","Coffee, ""local""\nshop","10.10","Food"\r\n' + + `"2026-09-02","","20.20",""${ending}`, + ); + assert.equal(report.count, 2); + assert.equal(report.spending, 30.3); + assert.equal(report.transactions[0].description, 'Coffee, "local"\nshop'); + assert.equal(report.transactions[1].description, ""); + assert.equal(report.transactions[1].category, "Uncategorized"); + } +});