Steps to reproduce
- Add an inventory item with the name =HYPERLINK("http://evil.example","click") (or any string starting with =, +, -, @).
- Sell that item (or add an expense with a similarly crafted description).
- Call GET /api/export/tax?year= and open the downloaded CSV in Excel or Google Sheets.
- Observe the formula executes/renders as a live hyperlink/formula instead of plain text — this is CSV Formula Injection (CWE-1236), a well-known attack vector for CSV exports containing untrusted data.
- Separately, add an item/expense with a " in the name (e.g. 6" Jordan sample) — observe the exported CSV row is now malformed because the embedded quote isn't escaped.
Expected behavior
- Any cell value starting with =, +, -, or @ should be neutralized before being written (standard mitigation: prefix with a single quote ' or a leading space, depending on convention) so spreadsheet software treats it as literal text, not a formula.
- Embedded double quotes in any field must be escaped by doubling (" → ""), per RFC 4180, so structure isn't broken by user-entered names/descriptions.
Actual behavior
Both inventoryItem.name/sku and expense.description are interpolated directly into the CSV string with a single wrapping "..." and no sanitization or escaping of their contents.
Proposed solution
- Add a small sanitizeCsvField(value: string): string helper in the same file (or ~/utils) that:
- Escapes internal " by doubling them.
- If the first character is =, +, -, @, tab, or CR, prefixes the value with a single quote ' to defuse formula interpretation.
- Apply it to every interpolated user-controlled field in both the Sale and Expense CSV-row builders (inventoryItem.name, inventoryItem.sku, e.description).
- Add a unit/integration test asserting a name like =1+1 is rendered as '=1+1 (or equivalent neutralized form) in the output stream, and a name containing " round-trips correctly.
Alternatives considered
Using a CSV-writing library (e.g. papaparse's unparse) instead of manual string building would handle escaping automatically, but given the file already streams rows incrementally via ReadableStream for memory efficiency with large exports, a small sanitize helper keeps that streaming design intact without pulling in a dependency just for escaping.
Additional Context
- This pattern (manual CSV row building without escaping) may also exist elsewhere if there are other CSV/export routes — happy to grep for and note them separately rather than bundle into this PR.
- I'd like to work on this issue under ECSoC26 — will open a PR only after approval/assignment by @rushikesh-bobade or another maintainer, since it's a security-labeled fix.
Steps to reproduce
Expected behavior
Actual behavior
Both inventoryItem.name/sku and expense.description are interpolated directly into the CSV string with a single wrapping "..." and no sanitization or escaping of their contents.
Proposed solution
Alternatives considered
Using a CSV-writing library (e.g. papaparse's unparse) instead of manual string building would handle escaping automatically, but given the file already streams rows incrementally via ReadableStream for memory efficiency with large exports, a small sanitize helper keeps that streaming design intact without pulling in a dependency just for escaping.
Additional Context