diff --git a/prowler/changelog.d/html-compliance-escape.security.md b/prowler/changelog.d/html-compliance-escape.security.md
new file mode 100644
index 0000000000..98b4b12a04
--- /dev/null
+++ b/prowler/changelog.d/html-compliance-escape.security.md
@@ -0,0 +1 @@
+HTML report compliance column is now escaped, so framework names and requirement IDs containing markup are shown as text instead of being rendered by the browser
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index 60428bede7..da3faa8b1c 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -116,7 +116,7 @@ class HTML(Output):
{str(escape(finding.status_extended)).replace("_", "_")} |
{HTML.process_markdown(str(escape(finding.metadata.Risk)))} |
{HTML.process_markdown(str(escape(finding.metadata.Remediation.Recommendation.Text)))} |
- {parse_html_string(unroll_dict(finding.compliance, separator=": "))} |
+ {parse_html_string(str(escape(unroll_dict(finding.compliance, separator=": "))))} |
""")
except Exception as error:
diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py
index 1590d29742..6c39e43249 100644
--- a/tests/lib/outputs/html/html_test.py
+++ b/tests/lib/outputs/html/html_test.py
@@ -910,6 +910,23 @@ class TestHTML:
assert "•key&<>'"<img" in output_data
assert "=value&<>'"<img" in output_data
+ def test_transform_escapes_compliance(self):
+ xss_payload = '
'
+ findings = [
+ generate_finding_output(
+ compliance={f"Framework{xss_payload}-1.0": [xss_payload, "1.1"]},
+ )
+ ]
+
+ output_data = HTML(findings).data[0]
+
+ assert xss_payload not in output_data
+ assert (
+ "•Framework<img src=x "
+ "onerror="window.PROWLER_COMPLIANCE_XSS=1">-1.0: "
+ "<img src=x onerror="window.PROWLER_COMPLIANCE_XSS=1">, 1.1"
+ ) in output_data
+
def test_transform_escapes_metadata_fields(self):
finding = generate_finding_output()
finding.metadata.Severity = MagicMock(value='
')