PYSEC-2026-3908
Vulnerability from pysec - Published: 2026-09-10 09:45 - Updated: 2026-09-10 11:02Summary
Prowler's HTML output formatter inserts finding.resource_tags into the generated report without HTML escaping. A cloud principal who can create or edit a resource tag in an account that is later scanned can store HTML or JavaScript in that tag. When another user opens the generated Prowler HTML report, the payload executes in the report page.
Impact
This is stored cross-site scripting in a security report artifact. The attacker needs permission to influence tags on a scanned cloud resource, and the victim must open the generated HTML report. In that context, JavaScript can read and modify the report DOM, alter displayed findings, and interact with any same-origin local or hosted report content available to the browser. This can undermine trust in generated security findings and can expose data contained in the report page.
Gate checks
The repository has 13,875 GitHub stars, which is above the 10,000 star floor. SECURITY.md is present and does not reject GitHub private reporting. The dedicated GitHub private vulnerability reporting endpoint for prowler-cloud/prowler returned true.
Affected versions
The vulnerable sink is present in current master at commit 329dfdf8e6cb8bc0424fb54b6595408e20969782 and in the latest GitHub release tag 5.28.0. The package metadata identifies the Python package as prowler.
Technical details
The HTML formatter builds each finding row with an f-string in prowler/lib/outputs/html/html.py. Neighboring fields are explicitly escaped:
resource_uidis escaped at line 85 with.replace("<", "<").replace(">", ">").status_extendedis escaped at line 87 with the same pattern.
The tag column does not apply equivalent escaping:
<td>{parse_html_string(unroll_dict(finding.resource_tags))}</td>
unroll_dict concatenates tag keys and values into a string, and parse_html_string only prefixes each item with an HTML bullet. Neither function escapes <, >, quotes, or event-handler attributes before insertion into the table cell. As a result, a tag value such as this remains active markup in the generated report:
<img src=x onerror="window.PROWLER_TAG_XSS=1">
Reproduction
- Create or identify a cloud resource that Prowler will include in an HTML report.
- Add a tag value containing a harmless proof payload, for example:
<img src=x onerror="window.PROWLER_TAG_XSS=1">
- Run Prowler against the account and generate HTML output.
- Open the generated HTML report in a browser.
- The payload executes from the tags column.
A proof HTML fragment using the same formatter structure set window.PROWLER_TAG_XSS=1 from resource_tags, while equivalent payloads in resource_uid and status_extended stayed text because those fields are escaped.
Local evidence:
/home/unkn0wn/security_audit/vulnerability-reports/prowler-html-tag-xss-evidence-2026-05-26.json/home/unkn0wn/security_audit/vulnerability-reports/prowler-html-tag-xss-proof-full.html
PoC
The minimized generated row below mirrors the vulnerable formatter output. The first and third candidate payloads are escaped controls. The middle payload is the unescaped resource_tags value and executes when the report is rendered.
<tr class="table-danger">
<td><img src=x onerror="window.PROWLER_UID_XSS=1"></td>
<td>
•Owner=<img src=x onerror="window.PROWLER_TAG_XSS=1;document.body.setAttribute('data-prowler-tag-xss','1')">
</td>
<td><img src=x onerror="window.PROWLER_STATUS_XSS=1"></td>
</tr>
Browser proof result from the local artifact:
{"tag": 1, "uid": 0, "status": 0, "attr": "1"}
Remediation
Escape tag keys and values before inserting them into HTML output. Prefer a single HTML escaping helper for every finding field that can originate from provider data. If bullets or separators are needed, build the list structure after escaping each key and value, rather than escaping the final HTML string after markup has been added.
| Name | purl | prowler-cloud | pkg:pypi/prowler-cloud |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "prowler-cloud",
"purl": "pkg:pypi/prowler-cloud"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "5.37.0"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.0.0rc1",
"0.0.0rc10",
"0.0.0rc11",
"0.0.0rc2",
"0.0.0rc3",
"0.0.0rc4",
"0.0.0rc5",
"0.0.0rc6",
"0.0.0rc7",
"0.0.0rc8",
"0.0.0rc9",
"3.0.0",
"3.0.0rc1",
"3.0.0rc11",
"3.0.0rc12",
"3.0.0rc13",
"3.0.0rc14",
"3.0.0rc15",
"3.0.1",
"3.0.2",
"3.1.0",
"3.1.1",
"3.1.2",
"3.1.3",
"3.1.4",
"3.10.0",
"3.11.0",
"3.11.1",
"3.11.2",
"3.11.3",
"3.12.0",
"3.12.1",
"3.13.0",
"3.13.1",
"3.14.0",
"3.15.0",
"3.15.1",
"3.15.2",
"3.15.3",
"3.16.0",
"3.16.1",
"3.16.10",
"3.16.11",
"3.16.12",
"3.16.13",
"3.16.14",
"3.16.15",
"3.16.17",
"3.16.2",
"3.16.3",
"3.16.4",
"3.16.5",
"3.16.6",
"3.16.7",
"3.16.8",
"3.16.9",
"3.2.0",
"3.2.1",
"3.2.2",
"3.2.3",
"3.2.4",
"3.3.0",
"3.3.1",
"3.3.2",
"3.3.3",
"3.3.4",
"3.4.0",
"3.4.1",
"3.5.0",
"3.5.1",
"3.5.2",
"3.5.3",
"3.6.0",
"3.6.1",
"3.7.0",
"3.7.1",
"3.7.2",
"3.8.0",
"3.8.1",
"3.8.2",
"3.9.0",
"4.0.0",
"4.0.1",
"4.1.0",
"4.2.0",
"4.2.1",
"4.2.2",
"4.2.3",
"4.2.4",
"4.3.0",
"4.3.1",
"4.3.2",
"4.3.3",
"4.3.4",
"4.3.5",
"4.3.6",
"4.3.7",
"4.4.0",
"4.4.1",
"4.5.0",
"4.5.1",
"4.5.2",
"4.5.3",
"4.6.0",
"4.6.1",
"4.6.2",
"5.0.0",
"5.0.1",
"5.0.2",
"5.0.3",
"5.0.4",
"5.0.5",
"5.1.0",
"5.1.1",
"5.1.2",
"5.1.3",
"5.1.4",
"5.1.5",
"5.10.0",
"5.10.1",
"5.10.2",
"5.11.0",
"5.12.0",
"5.12.1",
"5.12.2",
"5.12.3",
"5.13.0",
"5.13.1",
"5.14.0",
"5.14.1",
"5.14.2",
"5.15.0",
"5.15.1",
"5.16.0",
"5.16.1",
"5.17.0",
"5.17.1",
"5.18.0",
"5.18.1",
"5.18.2",
"5.18.3",
"5.19.0",
"5.2.0",
"5.2.1",
"5.2.2",
"5.2.3",
"5.20.0",
"5.21.0",
"5.21.1",
"5.22.0",
"5.23.0",
"5.24.0",
"5.24.1",
"5.24.2",
"5.24.3",
"5.24.4",
"5.25.0",
"5.25.1",
"5.25.2",
"5.25.3",
"5.26.0",
"5.26.1",
"5.27.0",
"5.27.1",
"5.28.0",
"5.28.1",
"5.29.0",
"5.29.1",
"5.29.2",
"5.3.0",
"5.30.0",
"5.30.1",
"5.30.2",
"5.30.3",
"5.31.0",
"5.31.1",
"5.32.0",
"5.32.1",
"5.33.0",
"5.33.1",
"5.33.2",
"5.34.0",
"5.35.0",
"5.36.0",
"5.4.0",
"5.4.1",
"5.4.2",
"5.4.3",
"5.4.4",
"5.5.1",
"5.6.0",
"5.7.0",
"5.7.1",
"5.7.2",
"5.7.3",
"5.7.4",
"5.7.5",
"5.8.0",
"5.8.1",
"5.9.0",
"5.9.1",
"5.9.2"
]
}
],
"aliases": [
"CVE-2026-73262",
"GHSA-c2jg-2778-ggm4"
],
"details": "## Summary\n\nProwler\u0027s HTML output formatter inserts `finding.resource_tags` into the generated report without HTML escaping. A cloud principal who can create or edit a resource tag in an account that is later scanned can store HTML or JavaScript in that tag. When another user opens the generated Prowler HTML report, the payload executes in the report page.\n\n## Impact\n\nThis is stored cross-site scripting in a security report artifact. The attacker needs permission to influence tags on a scanned cloud resource, and the victim must open the generated HTML report. In that context, JavaScript can read and modify the report DOM, alter displayed findings, and interact with any same-origin local or hosted report content available to the browser. This can undermine trust in generated security findings and can expose data contained in the report page.\n\n## Gate checks\n\nThe repository has 13,875 GitHub stars, which is above the 10,000 star floor. `SECURITY.md` is present and does not reject GitHub private reporting. The dedicated GitHub private vulnerability reporting endpoint for `prowler-cloud/prowler` returned `true`.\n\n## Affected versions\n\nThe vulnerable sink is present in current `master` at commit `329dfdf8e6cb8bc0424fb54b6595408e20969782` and in the latest GitHub release tag `5.28.0`. The package metadata identifies the Python package as `prowler`.\n\n## Technical details\n\nThe HTML formatter builds each finding row with an f-string in `prowler/lib/outputs/html/html.py`. Neighboring fields are explicitly escaped:\n\n- `resource_uid` is escaped at line 85 with `.replace(\"\u003c\", \"\u0026lt;\").replace(\"\u003e\", \"\u0026gt;\")`.\n- `status_extended` is escaped at line 87 with the same pattern.\n\nThe tag column does not apply equivalent escaping:\n\n```python\n\u003ctd\u003e{parse_html_string(unroll_dict(finding.resource_tags))}\u003c/td\u003e\n```\n\n`unroll_dict` concatenates tag keys and values into a string, and `parse_html_string` only prefixes each item with an HTML bullet. Neither function escapes `\u003c`, `\u003e`, quotes, or event-handler attributes before insertion into the table cell. As a result, a tag value such as this remains active markup in the generated report:\n\n```html\n\u003cimg src=x onerror=\"window.PROWLER_TAG_XSS=1\"\u003e\n```\n\n## Reproduction\n\n1. Create or identify a cloud resource that Prowler will include in an HTML report.\n2. Add a tag value containing a harmless proof payload, for example:\n\n```html\n\u003cimg src=x onerror=\"window.PROWLER_TAG_XSS=1\"\u003e\n```\n\n3. Run Prowler against the account and generate HTML output.\n4. Open the generated HTML report in a browser.\n5. The payload executes from the tags column.\n\nA proof HTML fragment using the same formatter structure set `window.PROWLER_TAG_XSS=1` from `resource_tags`, while equivalent payloads in `resource_uid` and `status_extended` stayed text because those fields are escaped.\n\nLocal evidence:\n\n- `/home/unkn0wn/security_audit/vulnerability-reports/prowler-html-tag-xss-evidence-2026-05-26.json`\n- `/home/unkn0wn/security_audit/vulnerability-reports/prowler-html-tag-xss-proof-full.html`\n\n## PoC\n\nThe minimized generated row below mirrors the vulnerable formatter output. The first and third candidate payloads are escaped controls. The middle payload is the unescaped `resource_tags` value and executes when the report is rendered.\n\n```html\n\u003ctr class=\"table-danger\"\u003e\n \u003ctd\u003e\u0026lt;img src=x onerror=\"window.PROWLER_UID_XSS=1\"\u0026gt;\u003c/td\u003e\n \u003ctd\u003e\n \u0026#x2022;Owner=\u003cimg src=x onerror=\"window.PROWLER_TAG_XSS=1;document.body.setAttribute(\u0027data-prowler-tag-xss\u0027,\u00271\u0027)\"\u003e\n \u003c/td\u003e\n \u003ctd\u003e\u0026lt;img src=x onerror=\"window.PROWLER_STATUS_XSS=1\"\u0026gt;\u003c/td\u003e\n\u003c/tr\u003e\n```\n\nBrowser proof result from the local artifact:\n\n```json\n{\"tag\": 1, \"uid\": 0, \"status\": 0, \"attr\": \"1\"}\n```\n\n## Remediation\n\nEscape tag keys and values before inserting them into HTML output. Prefer a single HTML escaping helper for every finding field that can originate from provider data. If bullets or separators are needed, build the list structure after escaping each key and value, rather than escaping the final HTML string after markup has been added.",
"id": "PYSEC-2026-3908",
"modified": "2026-09-10T11:02:29.359557Z",
"published": "2026-09-10T09:45:00.554532Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/prowler-cloud/prowler/security/advisories/GHSA-c2jg-2778-ggm4"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73262"
},
{
"type": "WEB",
"url": "https://github.com/prowler-cloud/prowler/pull/12221"
},
{
"type": "WEB",
"url": "https://github.com/prowler-cloud/prowler/commit/6db407ed3c17d4c73a8f619fdb30580c8465027f"
},
{
"type": "PACKAGE",
"url": "https://github.com/prowler-cloud/prowler"
},
{
"type": "WEB",
"url": "https://github.com/prowler-cloud/prowler/releases/tag/5.37.0"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/prowler-cloud"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-c2jg-2778-ggm4"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Prowler: Stored XSS in HTML reports through unescaped cloud resource tags"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.