Skip to content

security: CSV/TSV output does not neutralize spreadsheet formula injection #380

Description

@codeforester

Problem

_delimited_value() in lib/python/base_cli/output.py:256-265 sanitizes CSV/TSV cells against
terminal injection but not against spreadsheet formula injection (CWE-1236). Its docstring states
the intent:

Delimited output is commonly piped into another process. Keep the normal csv module's quoting
behavior, but remove ANSI/control sequences so a producer cannot inject terminal presentation
or unexpectedly split a record across physical lines.

It calls _table_cell(), which strips ANSI escapes and Cc/Cf category characters only. A cell
beginning with =, +, -, or @ is emitted verbatim; when the CSV is later opened in Excel,
LibreOffice, or Google Sheets, the value is evaluated as a formula. render_records() is the
public, documented way to emit csv/tsv, and resolve_output_format() makes tsv the
default for non-TTY output, so redirected output — the case most likely to be opened in a
spreadsheet or fed to another tool — is exactly the exposed path.

For the operations/SRE audience this matters because cell values routinely come from untrusted or
semi-trusted sources: cloud resource tags and names, Kubernetes annotations, git author fields,
ticket titles, alert payloads.

Verified evidence

Reviewed 2026-09-30 at a58ec109349fa3f3d03eae5b0de078b39ea361a2.

render_records(
    [{"name": "=cmd|'/C calc'!A1", "note": "+1+1", "x": "-2+3", "y": "@SUM(A1)"}],
    requested_format="csv",
    columns=[("NAME","name"),("NOTE","note"),("X","x"),("Y","y")],
    stream=buf,
)

Output:

"=cmd|'/C calc'!A1,+1+1,-2+3,@SUM(A1)\n"

Every one of the four standard formula-injection prefixes survives.

Proposal

Neutralize formula-leading cells in delimited output only (never in json, yaml, ndjson, or
the terminal table, where the concern does not apply and mangling values would be wrong):

  • prefix cells whose first character is one of = + - @ \t \r with a single ', or
  • wrap them so the csv module quotes them and the leading character is no longer first.

Make the behaviour explicit and overridable — a formula_guard: bool = True keyword on
render_records() — so a consumer producing machine-only CSV for its own parser can opt out with
an audited decision. Document it in docs/output-contracts.md next to the existing ANSI note.

Acceptance criteria

  • csv and tsv output neutralizes leading =, +, -, @, tab, and CR in every cell,
    including header cells.
  • json, yaml, ndjson, and terminal table rendering are byte-for-byte unchanged.
  • A regression test covers each prefix and asserts round-trip through csv.reader still yields
    the guarded value.
  • The guard is documented, including how to disable it and why that is a security decision.

Non-goals

  • Do not alter the csv module's quoting rules.
  • Do not apply the guard to structured formats where values are not interpreted as formulas.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

securitySecurity hardening or vulnerability work

Type

No type

Projects

  • Status
    In Progress

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions