fix(security): defuse CSV formula injection in multiline exécution cell + HTML-escape Markdown table cells
Finding 1 — CSV multiline formula injection: - Split _format_execution into _format_execution_text (MD/PDF, no sanitization) and _format_execution_csv (CSV, applies _csv_safe to each user-controlled component before join) - Moved _CSV_FORMULA_TRIGGERS + _csv_safe above the format helpers (required by _format_execution_csv) - Outer _csv_safe on the Exécution cell retained as belt-and-braces for the empty-date case - New test: test_render_engagement_csv_defuses_formula_in_inner_execution_lines Finding 2 — Stored XSS in Markdown table: - _cell() in render_engagement_markdown now calls _html_escape() (quote=True, default) before pipe-escaping and \n→<br/> substitution — correct order preserved - New test: test_render_engagement_markdown_escapes_html_in_table_cells 255 → 257 passed, ruff clean, mypy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,23 @@ def test_render_engagement_csv_escapes_formula_injection_in_execution(app) -> No
|
||||
assert "HYPERLINK" in cells[4]
|
||||
|
||||
|
||||
def test_render_engagement_csv_defuses_formula_in_inner_execution_lines(app) -> None:
|
||||
"""When executed_at is set, the cell starts with a safe date, but commands
|
||||
line may inject formulas. Each user-controlled component must be defused."""
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
sim = _make_sim(
|
||||
executed_at=datetime(2026, 6, 1, 10, 0, tzinfo=UTC),
|
||||
commands="=cmd|'/c calc'!A1",
|
||||
execution_result="@SUM(1)",
|
||||
)
|
||||
result = render_engagement_csv(eng, [sim])
|
||||
cells = list(_csv.reader(_io.StringIO(result)))[1]
|
||||
execution_cell = cells[4] # Exécution column
|
||||
assert "'=cmd|'/c calc'!A1" in execution_cell
|
||||
assert "'@SUM(1)" in execution_cell
|
||||
|
||||
|
||||
def test_render_engagement_csv_does_not_alter_safe_strings(app) -> None:
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
@@ -230,6 +247,24 @@ def test_render_engagement_csv_does_not_alter_safe_strings(app) -> None:
|
||||
assert "whoami /all" in cells[4]
|
||||
|
||||
|
||||
def test_render_engagement_markdown_escapes_html_in_table_cells(app) -> None:
|
||||
"""User content in table cells must be HTML-escaped to prevent stored XSS
|
||||
when the .md is opened in a renderer that interprets inline HTML."""
|
||||
with app.app_context():
|
||||
eng = _make_engagement()
|
||||
sim = _make_sim(
|
||||
name="<script>alert(1)</script>",
|
||||
commands='<img src=x onerror="alert(1)">',
|
||||
)
|
||||
result = render_engagement_markdown(eng, [sim])
|
||||
assert "<script>" not in result
|
||||
assert 'onerror="alert' not in result
|
||||
assert "<script>" in result
|
||||
assert "<img" in result
|
||||
# double-quotes in attribute values are also escaped
|
||||
assert """ in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PDF tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user