Skip to content

Commit 9fd013d

Browse files
committed
fix(serve): hide exception details in web errors
1 parent 3818087 commit 9fd013d

3 files changed

Lines changed: 44 additions & 19 deletions

File tree

src/serve/routes.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,9 @@ async def approve_campaign_action(request: Request, packet_id: str, idx: int) ->
404404
output_dir = _output_dir(request)
405405
try:
406406
_approve_action(packet_id, idx, output_dir)
407-
except (ValueError, IndexError) as exc:
407+
except (ValueError, IndexError):
408408
return HTMLResponse(
409-
f'<tr><td colspan="6" class="error">Not found: {_escape(exc)}</td></tr>',
409+
'<tr><td colspan="6" class="error">Not found.</td></tr>',
410410
status_code=404,
411411
)
412412

@@ -441,9 +441,9 @@ async def reject_campaign_action(
441441
output_dir = _output_dir(request)
442442
try:
443443
_reject_action(packet_id, idx, output_dir, reason=reason)
444-
except (ValueError, IndexError) as exc:
444+
except (ValueError, IndexError):
445445
return HTMLResponse(
446-
f'<tr><td colspan="6" class="error">Not found: {_escape(exc)}</td></tr>',
446+
'<tr><td colspan="6" class="error">Not found.</td></tr>',
447447
status_code=404,
448448
)
449449

@@ -573,9 +573,9 @@ async def approve_section_route(request: Request, record_id: str) -> HTMLRespons
573573
output_dir = _output_dir(request)
574574
try:
575575
_approve_section(record_id, output_dir)
576-
except ValueError as exc:
576+
except ValueError:
577577
return HTMLResponse(
578-
f'<div class="section-card section-card--error">Not found: {_escape(exc)}</div>',
578+
'<div class="section-card section-card--error">Not found.</div>',
579579
status_code=404,
580580
)
581581

@@ -605,9 +605,9 @@ async def reject_section_route(
605605
output_dir = _output_dir(request)
606606
try:
607607
_reject_section(record_id, output_dir, reason=reason)
608-
except ValueError as exc:
608+
except ValueError:
609609
return HTMLResponse(
610-
f'<div class="section-card section-card--error">Not found: {_escape(exc)}</div>',
610+
'<div class="section-card section-card--error">Not found.</div>',
611611
status_code=404,
612612
)
613613

@@ -781,9 +781,9 @@ async def accept_initiative_route(
781781

782782
try:
783783
truth = json.loads(truth_path.read_text())
784-
except (OSError, json.JSONDecodeError) as exc:
784+
except (OSError, json.JSONDecodeError):
785785
return HTMLResponse(
786-
f'<div class="suggestion-card accept-error">Error: failed to read portfolio-truth: {_escape(exc)}</div>',
786+
'<div class="suggestion-card accept-error">Error: failed to read portfolio truth.</div>',
787787
status_code=400,
788788
)
789789

@@ -797,9 +797,9 @@ async def accept_initiative_route(
797797
deadline=deadline,
798798
target_tier=target_tier,
799799
)
800-
except ValueError as exc:
800+
except ValueError:
801801
return HTMLResponse(
802-
f'<div class="suggestion-card accept-error">Error: {_escape(exc)}</div>',
802+
'<div class="suggestion-card accept-error">Error: unable to accept initiative.</div>',
803803
status_code=400,
804804
)
805805

@@ -826,9 +826,9 @@ async def dismiss_suggestion_route(
826826
entry = dismiss_suggestion_record(
827827
dismissed_path(output_dir), repo_name=repo_name, reason=reason
828828
)
829-
except ValueError as exc:
829+
except ValueError:
830830
return HTMLResponse(
831-
f'<div class="suggestion-card accept-error">Error: {_escape(exc)}</div>',
831+
'<div class="suggestion-card accept-error">Error: unable to dismiss suggestion.</div>',
832832
status_code=400,
833833
)
834834

tests/test_initiatives_suggestions_route.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,10 @@ def test_re_accepting_same_repo_is_idempotent(
319319
matching = [i for i in data["initiatives"] if i["repo_name"] == "IdempotentRepo"]
320320
assert len(matching) == 1 # upserted, not duplicated
321321

322-
def test_xss_repo_name_is_escaped_in_error(self, output_dir: Path, client: TestClient) -> None:
323-
"""XSS payload in repo_name is HTML-escaped in error response."""
322+
def test_xss_repo_name_is_not_echoed_in_error(
323+
self, output_dir: Path, client: TestClient
324+
) -> None:
325+
"""XSS payload in repo_name is not reflected in the error response."""
324326
truth = _make_portfolio_truth([_bronze_repo("SafeRepo")])
325327
(output_dir / "portfolio-truth-latest.json").write_text(json.dumps(truth))
326328

@@ -335,10 +337,9 @@ def test_xss_repo_name_is_escaped_in_error(self, output_dir: Path, client: TestC
335337
)
336338

337339
assert resp.status_code == 400
338-
# Raw script tag must NOT appear verbatim
339340
assert "<script>" not in resp.text
340-
# Escaped form should be present
341-
assert "&lt;script&gt;" in resp.text
341+
assert "&lt;script&gt;" not in resp.text
342+
assert "unable to accept initiative" in resp.text
342343

343344

344345
# ── Nav link ─────────────────────────────────────────────────────────────────

tests/test_serve.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,30 @@ def test_section_card_values_are_escaped(self) -> None:
573573
assert "&lt;h1&gt;bad&lt;/h1&gt;" in html
574574
assert "&lt;script&gt;alert(1)&lt;/script&gt;" in html
575575

576+
def test_campaign_action_error_hides_exception_details(self, client: TestClient) -> None:
577+
with patch(
578+
"src.plan_campaign.approve_action",
579+
side_effect=ValueError("internal stack trace /tmp/private.py:99"),
580+
):
581+
resp = client.post("/approvals/packet-1/actions/0/approve")
582+
583+
assert resp.status_code == 404
584+
assert "Not found." in resp.text
585+
assert "internal stack trace" not in resp.text
586+
assert "/tmp/private.py" not in resp.text
587+
588+
def test_section_error_hides_exception_details(self, client: TestClient) -> None:
589+
with patch(
590+
"src.draft_readmes.approve_section",
591+
side_effect=ValueError("internal stack trace /tmp/private.py:99"),
592+
):
593+
resp = client.post("/approvals/sections/section-1/approve")
594+
595+
assert resp.status_code == 404
596+
assert "Not found." in resp.text
597+
assert "internal stack trace" not in resp.text
598+
assert "/tmp/private.py" not in resp.text
599+
576600

577601
# ---------------------------------------------------------------------------
578602
# CLI wiring smoke test (no server start)

0 commit comments

Comments
 (0)