fix(security): defuse CSV formula injection in engagement export (MEDIUM)
Authenticated red-team users could craft any user-controlled string field (name, description, commands, prerequisites, execution_result, log_source, logs, soc_comment, incident_number, MITRE technique IDs) starting with =, +, -, @, \t or \r. When the SOC analyst opens the exported CSV in Excel / LibreOffice / Google Sheets — explicitly the consumption flow this sprint optimizes for — the spreadsheet executes the field as a formula on the SOC's machine. Fix: new helper _csv_safe() prefixes a single apostrophe to any string starting with a formula-trigger character, forcing the spreadsheet to render the cell as text. Applied to every user-controlled field in render_engagement_csv. Numeric and ISO-date fields are not wrapped. Tests: - test_render_engagement_csv_escapes_formula_injection_in_name - test_render_engagement_csv_escapes_formula_injection_in_commands - test_render_engagement_csv_does_not_alter_safe_strings Result: 249 → 252 passing (the 1 remaining failure is pre-existing test_index_without_built_frontend_returns_json, unrelated to this fix). Flagged by security-guidance@claude-code-plugins automated review.
This commit is contained in:
@@ -202,3 +202,48 @@ def test_render_engagement_markdown_with_mitre_bundle_not_loaded_does_not_crash(
|
||||
finally:
|
||||
mitre_svc.mitre_loaded = original
|
||||
mitre_svc._tactics_by_technique = original_tactics
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CSV formula injection defense (security fix — 2026-06-08)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
import csv as _csv # noqa: E402 (sectioned import to keep test diff localized)
|
||||
import io as _io # noqa: E402
|
||||
|
||||
|
||||
def _parse_csv_data_row(csv_text: str, row_index: int = 1) -> list[str]:
|
||||
"""Return cells of row N (0=header) parsed by csv.reader to handle multilines."""
|
||||
return list(_csv.reader(_io.StringIO(csv_text)))[row_index]
|
||||
|
||||
|
||||
def test_render_engagement_csv_escapes_formula_injection_in_name(app) -> None:
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
sim = _make_sim(name="=cmd|'/c calc'!A1")
|
||||
result = render_engagement_csv(eng, [sim])
|
||||
cells = _parse_csv_data_row(result)
|
||||
# name is column index 1 (after id)
|
||||
assert cells[1] == "'=cmd|'/c calc'!A1", (
|
||||
"name beginning with = must be apostrophe-prefixed to defuse Excel formula"
|
||||
)
|
||||
|
||||
|
||||
def test_render_engagement_csv_escapes_formula_injection_in_commands(app) -> None:
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
sim = _make_sim(commands="@SUM(1+1)")
|
||||
result = render_engagement_csv(eng, [sim])
|
||||
cells = _parse_csv_data_row(result)
|
||||
# commands is column index 6 per _CSV_HEADERS order
|
||||
assert cells[6].startswith("'@"), "commands beginning with @ must be apostrophe-prefixed"
|
||||
|
||||
|
||||
def test_render_engagement_csv_does_not_alter_safe_strings(app) -> None:
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
sim = _make_sim(name="Mimikatz LSASS Dump", commands="whoami /all")
|
||||
result = render_engagement_csv(eng, [sim])
|
||||
cells = _parse_csv_data_row(result)
|
||||
assert cells[1] == "Mimikatz LSASS Dump", "safe name must not be modified"
|
||||
assert cells[6] == "whoami /all", "safe commands must not be modified"
|
||||
|
||||
Reference in New Issue
Block a user