diff --git a/backend/app/core/notifications.py b/backend/app/core/notifications.py
index cd9e4de..4ca40ff 100644
--- a/backend/app/core/notifications.py
+++ b/backend/app/core/notifications.py
@@ -1285,7 +1285,7 @@ def render_generation_session_report_html(
# Summary section
html_parts.append('
')
html_parts.append('
Summary
')
- html_parts.append(f'
Specification: {spec_path}
')
+ html_parts.append(f'
Specification: {html.escape(spec_path)}
')
html_parts.append(f'
Run ID: {generation_id}
')
html_parts.append(
f'
{variance.label}: '
diff --git a/backend/test/api/test_email_notifications.py b/backend/test/api/test_email_notifications.py
index f522c34..d6e2acb 100644
--- a/backend/test/api/test_email_notifications.py
+++ b/backend/test/api/test_email_notifications.py
@@ -598,6 +598,26 @@ def test_html_builder_is_the_single_source_used_by_email(
assert direct_html == via_notifier_html
assert direct_plain == via_notifier_plain
+ def test_spec_path_is_html_escaped(
+ self, mock_db, sample_workspace_docs, sample_estimation_result
+ ):
+ """spec_path is a free-form, caller-suppliable string — it must not be
+ interpolated into the HTML report unescaped (XSS)."""
+ from app.core.notifications import render_generation_session_report_html
+
+ workspace_ids, _ = sample_workspace_docs
+
+ html_content, _ = render_generation_session_report_html(
+ generation_id="est-test-123",
+ workspace_ids=workspace_ids,
+ result=sample_estimation_result,
+ spec_path="",
+ db=mock_db,
+ )
+
+ assert "" not in html_content
+ assert "<script>alert(1)</script>" in html_content
+
class TestMultiWorkspaceResultSerialization:
"""Stored Firestore result shape for email resend / P10Y metadata."""