diff --git a/prowler/changelog.d/html-report-xss.security.md b/prowler/changelog.d/html-report-xss.security.md
new file mode 100644
index 0000000000..e500556594
--- /dev/null
+++ b/prowler/changelog.d/html-report-xss.security.md
@@ -0,0 +1 @@
+HTML reports escape provider-originated finding fields to prevent stored cross-site scripting through malicious cloud resource tags
diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py
index 3463014232..675811951d 100644
--- a/prowler/lib/outputs/html/html.py
+++ b/prowler/lib/outputs/html/html.py
@@ -1,5 +1,7 @@
+import re
import sys
from io import TextIOWrapper
+from urllib.parse import urlparse
import markdown
from markupsafe import escape
@@ -16,6 +18,33 @@ from prowler.lib.outputs.output import Finding, Output
from prowler.lib.outputs.utils import parse_html_string, unroll_dict
from prowler.providers.common.provider import Provider
+_SAFE_URL_SCHEMES = {"http", "https"}
+
+
+def _safe_url(url: str) -> str:
+ """Return url if its scheme is http/https, otherwise return empty string."""
+ if not url:
+ return ""
+ scheme = urlparse(url).scheme.lower()
+ return url if scheme in _SAFE_URL_SCHEMES else ""
+
+
+def _strip_unsafe_links(html_content: str) -> str:
+ """Replace tags whose href is not http/https with their link text."""
+
+ def _replace(match: re.Match) -> str:
+ href = match.group("href")
+ body = match.group("body")
+ safe = _safe_url(href)
+ return f'{body}' if safe else body
+
+ return re.sub(
+ r']*href="(?P
", "") html_content = html_content.replace("
", "") - return html_content + return _strip_unsafe_links(html_content) def transform(self, findings: list[Finding]) -> None: """Transforms the findings into the HTML format. @@ -77,16 +106,16 @@ class HTML(Output): self._data.append(f"""{HTML.process_markdown(finding.metadata.Risk)}
{HTML.process_markdown(finding.metadata.Remediation.Recommendation.Text)}
{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=": "))}
monitored aliases" in output_data
assert "