Skip to content

Prevent formula injection in CSV/XLSX exports#928

Open
pquerna wants to merge 1 commit into
mainfrom
claude/autopatch-scan-df67154c-vuln-1285532-MWaR4
Open

Prevent formula injection in CSV/XLSX exports#928
pquerna wants to merge 1 commit into
mainfrom
claude/autopatch-scan-df67154c-vuln-1285532-MWaR4

Conversation

@pquerna

@pquerna pquerna commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Add sanitization to exported CSV and XLSX files to prevent spreadsheet formula injection attacks. User-controlled data from upstream identity systems is now validated before export to prevent malicious formulas from being executed when files are opened in Excel, Google Sheets, LibreOffice, or other spreadsheet applications.

Changes

  • New sanitizeCell() function in export.go: Removes control characters (tabs, carriage returns, newlines) and prefixes cells starting with =, +, -, or @ with a single quote to force literal text rendering
  • CSV export (csv.go): Wrap all 12 cell values in the csvRow.Row() method with sanitizeCell()
  • XLSX export (xlsx.go): Wrap all 12 cell values in the excelRow.Row() method with sanitizeCell()

Implementation Details

The sanitizeCell() function:

  1. Strips embedded control characters using strings.Map() with unicode.IsControl()
  2. Checks if the resulting string begins with a formula indicator (=, +, -, @)
  3. Prefixes with a single quote if needed, which spreadsheet applications interpret as a literal text marker
  4. Returns the sanitized string

This approach ensures that attacker-controllable values (e.g., user display names, resource names) cannot be weaponized as formulas when exported data is opened in spreadsheet applications.

https://claude.ai/code/session_01Jxcdh6eSR6V8JX9G6iSefs

User-controlled identity fields (names, IDs, emails, entitlement and
resource metadata) were written directly into CSV and XLSX exports.
Spreadsheet applications interpret a cell beginning with =, +, -, or @
as a formula, allowing an attacker who controls an upstream identity
field to inject formulas (e.g. HYPERLINK exfiltration) that execute when
an operator opens the report.

Add a shared sanitizeCell helper that strips control characters and
prefixes formula-leading values with a single quote, and apply it to
every string cell in both the CSV writer and the XLSX exporter.
@pquerna
pquerna requested a review from a team June 8, 2026 02:35
Comment thread cmd/baton/export.go
// single quote so the spreadsheet renders it as literal text. Embedded control
// characters — including tabs, carriage returns, and newlines that could be
// used to smuggle content or lead a formula — are stripped first.
func sanitizeCell(s string) string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: sanitizeCell is a security-sensitive function — consider adding unit tests covering key edge cases: empty string, strings starting with each trigger character (=, +, -, @), strings with embedded control characters, and strings that are already clean. This ensures regressions are caught if the function is modified later.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

General PR Review: Prevent formula injection in CSV/XLSX exports

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Review mode: full
View review run

Review Summary

This PR adds a sanitizeCell() function that strips control characters and prefixes formula-indicator characters (=, +, -, @) with a single quote, then applies it to all 12 fields in both CSV and XLSX export paths. The implementation follows OWASP best practices for spreadsheet formula injection prevention. No security or correctness issues found.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • cmd/baton/export.go:18 — Missing unit tests for sanitizeCell(), a security-sensitive function.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `cmd/baton/export.go`:
- Around line 18-34: The `sanitizeCell` function has no unit tests. Add a test file
  `cmd/baton/export_test.go` with table-driven tests covering: empty string returns empty,
  strings starting with `=`, `+`, `-`, `@` get a single-quote prefix, strings with embedded
  control characters (tabs, newlines, carriage returns) have them stripped, strings with both
  control chars and formula-trigger prefixes get both treatments, and clean strings pass through
  unchanged.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@gontzess gontzess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. [issue] XLSX doesn't need the quote-prefix, and it corrupts data. excelRow.Row writes values via f.SetCellValue(sheet, cell, ...). For a Go string, excelize routes that to SetCellStr, which writes a shared-string cell (type s) and removes any formula — it never creates a formula. Excel renders shared strings as literal text, so the XLSX path was not vulnerable to formula injection. Because excelize stores the value verbatim and never sets the quotePrefix style, the leading ' becomes part of the cell's data: @handle'@handle, -5'-5. Net result is altered data with no security gain. Suggest dropping sanitizeCell on the XLSX path (string cells are already safe), or, if you want defensive hardening, set the cell's quotePrefix style rather than mutating the string.

2. [suggestion] No unit test for sanitizeCell. It's a pure function — a small table test would lock the behavior: each prefix (= + - @) gets quoted, control chars stripped, "\t=1+1""'=1+1", empty string, and a multibyte/no-op leading char.

3. [suggestion] Full-width Unicode prefixes aren't caught. The first-byte check only matches ASCII. OWASP's CSV-injection guidance also lists full-width variants (= + - @, U+FF1D etc.) as test cases; a value starting with one slips through. Whether that's exploitable depends on the target app's Unicode normalization — worth a conscious decision rather than leaving it uncovered.

4. [nit] Control-char stripping removes more than needed. unicode.IsControl strips every control rune. Go's csv.Writer already quote-escapes embedded newlines/tabs, so stripping isn't required for CSV correctness — it just drops legitimate whitespace from names/descriptions. Fine as defense-in-depth, but a conscious tradeoff.

CSV approach is otherwise sound: control-char stripping covers tab/CR/LF, the = + - @ set matches OWASP, and csv.Writer prevents separator breakout.

ggreer
ggreer previously approved these changes Jun 9, 2026
@ggreer
ggreer dismissed their stale review June 9, 2026 23:57

Noticed Steve's comment.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants