mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
fix(html): escape provider identity fields in report header (#12424)
Co-authored-by: pedrooot <pedromarting3@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pedrooot <56402503+pedrooot@users.noreply.github.com>
This commit is contained in:
co-authored by
pedrooot
copilot-swe-agent[bot]
pedrooot
parent
de64df11b9
commit
02df22ca19
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
import sys
|
||||
from io import StringIO
|
||||
|
||||
import pytest
|
||||
from mock import MagicMock, patch
|
||||
|
||||
from prowler.config.config import prowler_version, timestamp
|
||||
@@ -676,6 +678,192 @@ html_footer = """
|
||||
"""
|
||||
|
||||
|
||||
def _setup_aws_xss(provider, payload):
|
||||
provider.identity.account = payload
|
||||
provider.identity.profile = payload
|
||||
provider.identity.audited_regions = [payload]
|
||||
provider.identity.user_id = payload
|
||||
provider.identity.identity_arn = payload
|
||||
|
||||
|
||||
def _setup_azure_xss(provider, payload):
|
||||
provider.identity.tenant_ids = [payload]
|
||||
provider.identity.tenant_domain = payload
|
||||
provider.identity.subscriptions = {payload: payload}
|
||||
provider.identity.identity_type = payload
|
||||
provider.identity.identity_id = payload
|
||||
|
||||
|
||||
def _setup_gcp_xss(provider, payload):
|
||||
provider.project_ids = [payload]
|
||||
provider.session._service_account_email = payload
|
||||
|
||||
|
||||
def _setup_kubernetes_xss(provider, payload):
|
||||
provider.identity.cluster = payload
|
||||
provider.identity.context = payload
|
||||
|
||||
|
||||
def _setup_github_xss(provider, payload):
|
||||
provider.identity = MagicMock(spec=["account_name", "account_email"])
|
||||
provider.identity.account_name = payload
|
||||
provider.identity.account_email = payload
|
||||
provider.auth_method = payload
|
||||
|
||||
|
||||
def _setup_m365_xss(provider, payload):
|
||||
provider.identity.tenant_domain = payload
|
||||
provider.identity.identity_type = payload
|
||||
provider.identity.identity_id = payload
|
||||
provider.identity.user = payload
|
||||
|
||||
|
||||
def _setup_nhn_xss(provider, payload):
|
||||
provider.identity.tenant_domain = payload
|
||||
provider.identity.identity_type = payload
|
||||
provider.identity.identity_id = payload
|
||||
|
||||
|
||||
def _setup_mongodbatlas_xss(provider, payload):
|
||||
provider.identity.organization_name = payload
|
||||
|
||||
|
||||
def _setup_iac_xss(provider, payload):
|
||||
provider.scan_repository_url = payload
|
||||
provider.scan_path = None
|
||||
provider.auth_method = payload
|
||||
|
||||
|
||||
def _setup_image_xss(provider, payload):
|
||||
provider.registry = payload
|
||||
provider.images = [payload]
|
||||
provider.auth_method = payload
|
||||
|
||||
|
||||
def _setup_llm_xss(provider, payload):
|
||||
provider.model = payload
|
||||
provider.plugins = [payload]
|
||||
provider.max_concurrency = payload
|
||||
provider.config_path = payload
|
||||
|
||||
|
||||
def _setup_oraclecloud_xss(provider, payload):
|
||||
provider.session.profile = payload
|
||||
provider.identity.tenancy_name = payload
|
||||
provider.identity.tenancy_id = payload
|
||||
|
||||
|
||||
def _setup_stackit_xss(provider, payload):
|
||||
provider.identity.project_id = payload
|
||||
provider.identity.project_name = payload
|
||||
provider.identity.audited_regions = {payload}
|
||||
|
||||
|
||||
def _setup_cloudflare_xss(provider, payload):
|
||||
account = MagicMock()
|
||||
account.id = payload
|
||||
provider.accounts = [account]
|
||||
provider.session.api_token = "token"
|
||||
provider.session.api_key = None
|
||||
provider.session.api_email = None
|
||||
provider.identity.email = payload
|
||||
|
||||
|
||||
def _setup_alibabacloud_xss(provider, payload):
|
||||
provider.identity.account_id = payload
|
||||
provider.identity.account_name = payload
|
||||
provider.identity.audited_regions = payload
|
||||
provider.identity.identity_arn = payload
|
||||
provider.identity.user_name = payload
|
||||
|
||||
|
||||
def _setup_openstack_xss(provider, payload):
|
||||
provider.identity.project_id = payload
|
||||
provider.identity.project_name = payload
|
||||
provider.identity.region_name = payload
|
||||
provider.identity.username = payload
|
||||
provider.identity.user_id = payload
|
||||
|
||||
|
||||
def _setup_googleworkspace_xss(provider, payload):
|
||||
provider.identity.domain = payload
|
||||
provider.identity.customer_id = payload
|
||||
provider.identity.delegated_user = payload
|
||||
|
||||
|
||||
def _setup_e2enetworks_xss(provider, payload):
|
||||
provider.identity.project_id = payload
|
||||
provider.identity.locations = [payload]
|
||||
|
||||
|
||||
def _setup_vercel_xss(provider, payload):
|
||||
team = MagicMock()
|
||||
team.name = payload
|
||||
team.id = payload
|
||||
provider.identity.team = team
|
||||
provider.identity.email = payload
|
||||
provider.identity.username = payload
|
||||
|
||||
|
||||
def _setup_okta_xss(provider, payload):
|
||||
provider.identity.org_domain = payload
|
||||
provider.auth_method = payload
|
||||
provider.identity.client_id = payload
|
||||
|
||||
|
||||
def _setup_scaleway_xss(provider, payload):
|
||||
provider.identity.organization_id = payload
|
||||
provider.session.access_key = payload
|
||||
provider.identity.bearer_type = payload
|
||||
provider.identity.bearer_email = payload
|
||||
provider.identity.bearer_id = payload
|
||||
provider.session.default_region = payload
|
||||
|
||||
|
||||
def _setup_linode_xss(provider, payload):
|
||||
provider.identity.username = payload
|
||||
provider.identity.email = payload
|
||||
provider.identity.account_id = payload
|
||||
|
||||
|
||||
def _setup_huaweicloud_xss(provider, payload):
|
||||
provider.identity.account_id = payload
|
||||
provider.identity.account_name = payload
|
||||
provider.identity.profile = payload
|
||||
provider.identity.regions = {payload}
|
||||
provider.identity.domain_id = payload
|
||||
provider.identity.user_id = payload
|
||||
provider.identity.user_name = payload
|
||||
provider.identity.identity_type = payload
|
||||
|
||||
|
||||
PROVIDER_XSS_SETUPS = [
|
||||
("aws", _setup_aws_xss),
|
||||
("azure", _setup_azure_xss),
|
||||
("gcp", _setup_gcp_xss),
|
||||
("kubernetes", _setup_kubernetes_xss),
|
||||
("github", _setup_github_xss),
|
||||
("m365", _setup_m365_xss),
|
||||
("nhn", _setup_nhn_xss),
|
||||
("mongodbatlas", _setup_mongodbatlas_xss),
|
||||
("iac", _setup_iac_xss),
|
||||
("image", _setup_image_xss),
|
||||
("llm", _setup_llm_xss),
|
||||
("oraclecloud", _setup_oraclecloud_xss),
|
||||
("stackit", _setup_stackit_xss),
|
||||
("cloudflare", _setup_cloudflare_xss),
|
||||
("alibabacloud", _setup_alibabacloud_xss),
|
||||
("openstack", _setup_openstack_xss),
|
||||
("googleworkspace", _setup_googleworkspace_xss),
|
||||
("e2enetworks", _setup_e2enetworks_xss),
|
||||
("vercel", _setup_vercel_xss),
|
||||
("okta", _setup_okta_xss),
|
||||
("scaleway", _setup_scaleway_xss),
|
||||
("linode", _setup_linode_xss),
|
||||
("huaweicloud", _setup_huaweicloud_xss),
|
||||
]
|
||||
|
||||
|
||||
class TestHTML:
|
||||
def test_transform_fail_finding(self):
|
||||
findings = [
|
||||
@@ -1100,6 +1288,103 @@ class TestHTML:
|
||||
assert "<script>alert(1)</script>" not in summary
|
||||
assert "Delhi"><script>alert(1)</script>" in summary
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider_type,setup_fn",
|
||||
PROVIDER_XSS_SETUPS,
|
||||
ids=[t for t, _ in PROVIDER_XSS_SETUPS],
|
||||
)
|
||||
def test_get_assessment_summary_escapes_provider_identity(
|
||||
self, provider_type, setup_fn
|
||||
):
|
||||
"""Every provider header must HTML-escape tenant-controlled identity fields."""
|
||||
payload = "<script>alert(1)</script>"
|
||||
findings = [generate_finding_output()]
|
||||
output = HTML(findings)
|
||||
|
||||
provider = MagicMock()
|
||||
provider.type = provider_type
|
||||
setup_fn(provider, payload)
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert payload not in summary
|
||||
assert "<script>alert(1)</script>" in summary
|
||||
|
||||
def test_provider_xss_setups_covers_every_assessment_summary_method(self):
|
||||
"""Adding a new get_<provider>_assessment_summary without a PROVIDER_XSS_SETUPS
|
||||
entry must fail this test, so the escape guarantee cannot silently regress."""
|
||||
pattern = re.compile(r"^get_(.+)_assessment_summary$")
|
||||
discovered = set()
|
||||
for name in dir(HTML):
|
||||
match = pattern.match(name)
|
||||
if match:
|
||||
discovered.add(match.group(1))
|
||||
|
||||
covered = {ptype for ptype, _ in PROVIDER_XSS_SETUPS}
|
||||
|
||||
missing = discovered - covered
|
||||
extra = covered - discovered
|
||||
assert (
|
||||
not missing
|
||||
), f"providers without XSS coverage in PROVIDER_XSS_SETUPS: {sorted(missing)}"
|
||||
assert (
|
||||
not extra
|
||||
), f"PROVIDER_XSS_SETUPS entries with no matching HTML method: {sorted(extra)}"
|
||||
|
||||
def test_github_app_get_assessment_summary_escapes_app_identity(self):
|
||||
"""The GitHub App branch (elif hasattr app_id) must escape app_name/app_id/installations,
|
||||
which the PAT setup does not exercise."""
|
||||
payload = "<script>alert(1)</script>"
|
||||
findings = [generate_finding_output()]
|
||||
output = HTML(findings)
|
||||
|
||||
provider = MagicMock()
|
||||
provider.type = "github"
|
||||
provider.identity = MagicMock(spec=["app_id", "app_name", "installations"])
|
||||
provider.identity.app_id = payload
|
||||
provider.identity.app_name = payload
|
||||
provider.identity.installations = [payload]
|
||||
provider.auth_method = payload
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert payload not in summary
|
||||
assert "<script>alert(1)</script>" in summary
|
||||
|
||||
def test_iac_get_assessment_summary_escapes_scan_path(self):
|
||||
"""The IAC scan_path branch (else of `if scan_repository_url`) must escape it."""
|
||||
payload = "<script>alert(1)</script>"
|
||||
findings = [generate_finding_output()]
|
||||
output = HTML(findings)
|
||||
|
||||
provider = MagicMock()
|
||||
provider.type = "iac"
|
||||
provider.scan_repository_url = None
|
||||
provider.scan_path = payload
|
||||
provider.auth_method = payload
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert payload not in summary
|
||||
assert "<script>alert(1)</script>" in summary
|
||||
|
||||
def test_image_get_assessment_summary_escapes_images_list(self):
|
||||
"""The Image `else` branch (no registry, images list) must escape each image."""
|
||||
payload = "<script>alert(1)</script>"
|
||||
findings = [generate_finding_output()]
|
||||
output = HTML(findings)
|
||||
|
||||
provider = MagicMock()
|
||||
provider.type = "image"
|
||||
provider.registry = None
|
||||
provider.images = [payload]
|
||||
provider.auth_method = payload
|
||||
|
||||
summary = output.get_assessment_summary(provider)
|
||||
|
||||
assert payload not in summary
|
||||
assert "<script>alert(1)</script>" in summary
|
||||
|
||||
def test_process_markdown_bold_text(self):
|
||||
"""Test that **text** is converted to <strong>text</strong>"""
|
||||
test_text = "This is **bold text** and this is **also bold**"
|
||||
|
||||
Reference in New Issue
Block a user