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:
Knacky
2026-06-08 18:13:16 +02:00
parent 25877c4092
commit 57dbd14347
2 changed files with 72 additions and 11 deletions

View File

@@ -154,6 +154,22 @@ _CSV_HEADERS = [
"updated_at",
]
_CSV_FORMULA_TRIGGERS = ("=", "+", "-", "@", "\t", "\r")
def _csv_safe(value: object) -> object:
"""Defuse spreadsheet formula injection by prefixing user-controlled cells.
Excel / LibreOffice / Google Sheets interpret cells starting with =, +, -, @,
\\t or \\r as formulas. Since this CSV is the engagement handoff to SOC and is
explicitly opened in a spreadsheet app, an authenticated red-team user could
craft a simulation field that executes on the SOC analyst's machine. Prefixing
with a single apostrophe forces the spreadsheet to treat the cell as text.
"""
if isinstance(value, str) and value and value[0] in _CSV_FORMULA_TRIGGERS:
return "'" + value
return value
def render_engagement_csv(
engagement: Engagement, simulations: list[Simulation]
@@ -169,19 +185,19 @@ def render_engagement_csv(
writer.writerow([
sim.id,
sim.name,
_csv_safe(sim.name),
sim.status.value,
tech_ids,
tactic_str,
sim.description or "",
sim.commands or "",
sim.prerequisites or "",
_csv_safe(tech_ids),
_csv_safe(tactic_str),
_csv_safe(sim.description or ""),
_csv_safe(sim.commands or ""),
_csv_safe(sim.prerequisites or ""),
sim.executed_at.isoformat() if sim.executed_at else "",
sim.execution_result or "",
sim.log_source or "",
sim.logs or "",
sim.soc_comment or "",
sim.incident_number or "",
_csv_safe(sim.execution_result or ""),
_csv_safe(sim.log_source or ""),
_csv_safe(sim.logs or ""),
_csv_safe(sim.soc_comment or ""),
_csv_safe(sim.incident_number or ""),
sim.created_at.isoformat() if sim.created_at else "",
sim.updated_at.isoformat() if sim.updated_at else "",
])