T09 · Insecure Skill Coding Practices
- Location
scripts/esg_exporter.py:229- Finding
Stored HTML Injection and Cross-Site Scripting in Generated Reports
- Content
View full analysis
Vulnerability Details
File Location:
scripts/esg_exporter.py, lines 229–260 and 304–352
Vulnerability Type: Stored HTML injection / cross-site scripting (XSS)
Risk Level: MediumVulnerable Code
python html_content = f"""<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{config.get('title', 'ESG report')}</title> ... </head> <body> <div class="container"> <div class="header"> <h1>{config.get('company_name', '')}</h1> <h2>{config.get('title', 'ESG sustainability report')}</h2> <p style="margin-top: 20px; color: #666;">Reporting period: {data.get('dimensions', {}).get('environmental', {}).get('E1', {}).get('period', '')}</p>The same unsafe interpolation pattern is used for all three ESG dimensions:
python for metric_id, info in data.get('dimensions', {}).get('environmental', {}).items(): if info.get('value') is not None: html_content += f""" <tr> <td>{info.get('name', metric_id)}</td> <td>{info.get('value')}</td> <td>{info.get('unit', '')}</td> <td>{info.get('period', '')}</td> </tr> """Equivalent code appears for social fields at lines 325–331 and governance fields at lines 346–352.
Technical Analysis
export_html_report()constructs an HTML document through direct f-string interpolation. It does not apply context-appropriate HTML escaping to the company name, report title, reporting period, metric name, metric value, or unit.The company name and title can originate from command-line arguments. Metric fields and periods originate from the input JSON; the reporting period may also be propagated from imported CSV or Excel data by the collector. Consequently, an attacker who can influence report input can inject HTML elements, event handlers, or script markup int ...[truncated 2147 chars]
- Remediation
View remediation
Remediation Suggestions
-
Apply context-appropriate HTML escaping to every dynamic value before interpolation. At minimum, use
html.escape(value, quote=True)after converting values to strings:python from html import escape def html_text(value): return escape(str(value if value is not None else ""), quote=True) -
Use the helper for the company name, title, reporting period, metric name, value, and unit:
python company_name = html_text(config.get("company_name", "")) title = html_text(config.get("title", "ESG sustainability report")) period = html_text( data.get("dimensions", {}) .get("environmental", {}) .get("E1", {}) .get("period", "") ) -
Prefer an auto-escaping template engine, such as Jinja2 with autoescape explicitly enabled for HTML templates. Avoid assembling HTML through repeated f-string concatenation.
-
Validate imported JSON against a strict schema. Enforce expected scalar types and reasonable maximum lengths for textual fields. Reject nested objects or unexpected data types where strings or numeric values are required.
-
Treat CSV and Excel input as untrusted, because fields such as
periodare propagated into the generated report. -
If reports are served from a website, deploy a restrictive Content Security Policy that disallows inline scripts and event handlers. This should be defense in depth rather than a replacement for output encoding.
-
Add regression tests for all dynamic HTML fields using payloads containing
<,>,&, quotes,<script>elements, and event-handler attributes. Verify that generated reports contain encoded text rather than executable markup.
-
