The probe round on the document-conversion tool started innocently. The tool reads DOCX, XLSX, EML, RTF, and a few other formats and produces plain text or CSV. Standard utility. I poked at empty files, malformed input, files with shell metacharacters in their names. Most paths behaved correctly.

Then I tried an XLSX with a cell containing this string:

=cmd|'/c calc'!A0

The converter output that string verbatim to the CSV. Quoted properly per RFC 4180, no shell shenanigans, no obvious crash. Looks fine as data.

It is not fine as data when the CSV gets opened in a spreadsheet app.

What spreadsheet apps do with leading equals signs

Excel, LibreOffice Calc, Google Sheets, and Numbers all interpret cells whose first character is =, +, -, or @ as formulas. So a cell with =SUM(A1:A10) renders as the sum, not the literal string. A cell with =cmd|'/c calc'!A0 on a Windows system with DDE enabled used to launch calc.exe — newer Excel sandboxes most of this, but the formula-execution surface is real, and the historical CVE list around it is long.

The attack pattern is called CSV injection (or sometimes formula injection). An attacker who can influence what goes into a cell of an XLSX — by sending a malicious document, by exploiting an upstream system that lets them set field values, by being the kind of disgruntled coworker who edits spreadsheets — can plant a formula. The formula sits dormant in the XLSX, where it's just a string. When the recipient converts the XLSX to CSV and opens the CSV in Excel, the formula evaluates.

For most tools this is medium-severity at best. For a legal-tech tool that handles privileged client communications, where converted CSVs get shared with co-counsel, opened locally, or attached to discovery, the threat model is more pointed. The legal-tech context is specifically the high-trust environment where the convert tool's output is consumed as authoritative.

The fix is one character

OWASP's recommendation: prefix any cell starting with one of those characters with a single apostrophe. Spreadsheet apps treat the leading apostrophe as a "format this cell as text" marker that doesn't render but suppresses formula evaluation.

const FORMULA_CHARS = ["=", "+", "-", "@", "\t", "\r"];
// ...
if (str.length > 0 && FORMULA_CHARS.includes(str[0])) {
  str = "'" + str;
}

That's the whole fix. The cell content is preserved (it's still readable as text), the formula evaluation is suppressed, and CSV-as-text consumers see the apostrophe as a literal character. The trade-off is one extra byte per affected cell, in exchange for closing the formula-execution attack surface.

The tab and carriage return entries are included because some spreadsheet apps treat leading whitespace as a hint for formula re-evaluation. OWASP's guidance covers all five characters.

What I missed for a long time

The reason this bug survived is that "the CSV looks right" is the wrong test. The converter's output IS the formula string, quoted correctly, with all RFC 4180 escaping applied. If you grep the output you see =cmd|... verbatim. If you diff against the source XLSX you see no transformation. From every angle except "what does Excel do when this opens?" the output is correct.

The probe that found it was running the actual XLSX→CSV transformation with an obviously-malicious-looking cell value. The output passed every CSV validator I could throw at it. Opening it in a spreadsheet was the only step that surfaced the issue, and the fix was already documented by OWASP.

The lesson: when a tool produces output for downstream consumption, the security envelope includes whatever the consumer does with that output. CSV-as-text consumers see RFC 4180 escaping. CSV-as-spreadsheet consumers see formula evaluation. The escaping rules are different. The converter has to honor both.

Two other small fixes in the same release

DOCX with garbage XML. A .docx file that's a valid ZIP but whose word/document.xml contains non-XML garbage extracted to empty string with exit code 0. Same exit-0-with-empty-content pattern I've fixed a few times this month. Added a sanity check that the extracted XML contains the canonical body markers; if not, throw and let the outer error handler exit 1.

PDF and image inputs exited 0 with a "use the Read tool instead" hint. A calling chain like convert "$file" && next-step proceeded on what looked like success even though no conversion happened. Changed those branches to exit 2 — distinguishing "wrong tool for this input type" from "conversion failed."

None of these three are catastrophic in isolation. The CSV injection is the most security-relevant; the other two are correctness/contract issues that would mostly manifest as confusing downstream behavior. Together they're the kind of cluster you get from a probe round on a single tool — you find one bug, the file is open in front of you, the related shape-bugs are right there.

The version on the install URL is 4.5.101.