Why it matters
app/api/export/route.ts builds the public CSV export. Its escapeCell() helper quotes cells that contain a comma, a double quote, or a newline, which is correct RFC 4180 escaping. But CSV files are usually opened in a spreadsheet, and a cell whose value begins with =, +, -, @, tab, or carriage return is interpreted by Excel, LibreOffice, and Google Sheets as a formula.
Case titles and outcomes in this export are user-submitted, so a submission titled =HYPERLINK("http://evil.example","click") ends up as a live formula in anyone's spreadsheet who downloads the export. This is the classic CSV injection issue.
What to change
In app/api/export/route.ts:
- In
escapeCell(), prefix any value whose first character is one of = + - @ \t \r with a single quote (or another agreed neutraliser) before the existing quoting logic runs.
- Add
\r to the set of characters that force quoting. A lone carriage return currently slips through.
- Add a test file
app/api/export/route.test.ts covering escapeCell behaviour. The repo already has route tests to copy the shape from, for example app/api/posts/route.test.ts and app/api/admin/posts/route.test.ts.
escapeCell is module-private today, so exporting it for the test is fine and is the smallest change.
Notes
- Run
npm test and npm run format before opening the PR.
Questions are very welcome. Comment here to claim it and ask anything you are unsure about, you will usually get a reply within a day.
Why it matters
app/api/export/route.tsbuilds the public CSV export. ItsescapeCell()helper quotes cells that contain a comma, a double quote, or a newline, which is correct RFC 4180 escaping. But CSV files are usually opened in a spreadsheet, and a cell whose value begins with=,+,-,@, tab, or carriage return is interpreted by Excel, LibreOffice, and Google Sheets as a formula.Case titles and outcomes in this export are user-submitted, so a submission titled
=HYPERLINK("http://evil.example","click")ends up as a live formula in anyone's spreadsheet who downloads the export. This is the classic CSV injection issue.What to change
In
app/api/export/route.ts:escapeCell(), prefix any value whose first character is one of= + - @ \t \rwith a single quote (or another agreed neutraliser) before the existing quoting logic runs.\rto the set of characters that force quoting. A lone carriage return currently slips through.app/api/export/route.test.tscoveringescapeCellbehaviour. The repo already has route tests to copy the shape from, for exampleapp/api/posts/route.test.tsandapp/api/admin/posts/route.test.ts.escapeCellis module-private today, so exporting it for the test is fine and is the smallest change.Notes
npm testandnpm run formatbefore opening the PR.Questions are very welcome. Comment here to claim it and ask anything you are unsure about, you will usually get a reply within a day.