Skip to content

CSV/Formula injection vulnerability in tax export (api.export.tax.ts) #138

Description

@shivi14112007-create

Steps to reproduce

  1. Add an inventory item with the name =HYPERLINK("http://evil.example","click") (or any string starting with =, +, -, @).
  2. Sell that item (or add an expense with a similarly crafted description).
  3. Call GET /api/export/tax?year= and open the downloaded CSV in Excel or Google Sheets.
  4. 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.
  5. 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.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions