T09 · Insecure Skill Coding Practices
- Location
scripts/export.py:150- Finding
Stored HTML Injection in Exported Reports
- Content
View full analysis
Vulnerability Details
File Location:
scripts/export.py, lines 150–166
Vulnerability Type: Stored HTML injection caused by missing output encoding
Risk Level: MediumVulnerable Code
python if goals.get('main_goal'): html += f" <h3>核心目标</h3>\n <p>{goals['main_goal']}</p>\n" if goals.get('sub_goals'): html += " <h3>子目标</h3>\n <ul>\n" for g in goals['sub_goals']: status = "✓" if g.get('completed') else "○" cls = "done" if g.get('completed') else "" html += f' <li class="{cls}">{status} {g["content"]}</li>\n' html += " </ul>\n" if goals.get('pending'): html += " <h3>待完成</h3>\n <ul>\n" for p in goals['pending']: html += f" <li>{p}</li>\n" html += " </ul>\n"The affected fields are derived from conversation content without sanitization. For example,
scripts/extract_goals.py, lines 37–53, returns user-controlled message text:python for msg in messages[:5]: if msg.get("role") != "user": continue content = msg.get("content", "") for keyword in GOAL_KEYWORDS: if keyword in content: sentences = re.split(r'[。.!?]', content) for sent in sentences: if keyword in sent and len(sent) > 5: return sent.strip(), "initial" if len(content) > 10: return content[:200], "initial"Technical Analysis
Exporter.to_html()constructs an HTML document by directly interpolating goal data into element bodies. Themain_goal,sub_goals[].content, andpending[]values can originate from untrusted conversation messages processed byextract_goals().No contextual HTML escaping is applied to these values. Consequently, HTML tags and event-handler attributes are interpreted as active markup rather than displayed as text. Although the summary field is partially escaped elsewhere in
to_html(), that protection does not cover the affected goal fie ...[truncated 1869 chars]- Remediation
View remediation
Remediation Suggestions
Apply contextual HTML escaping to every dynamic value before inserting it into the report:
python from html import escape if goals.get("main_goal"): main_goal = escape(str(goals["main_goal"])) html += f" <h3>Core Goal</h3>\n <p>{main_goal}</p>\n" if goals.get("sub_goals"): html += " <h3>Sub-goals</h3>\n <ul>\n" for goal in goals["sub_goals"]: status = "✓" if goal.get("completed") else "○" css_class = "done" if goal.get("completed") else "" safe_class = escape(css_class, quote=True) safe_status = escape(status) safe_content = escape(str(goal.get("content", ""))) html += ( f' <li class="{safe_class}">' f"{safe_status} {safe_content}</li>\n" ) html += " </ul>\n" if goals.get("pending"): html += " <h3>Pending</h3>\n <ul>\n" for item in goals["pending"]: html += f" <li>{escape(str(item))}</li>\n" html += " </ul>\n"Additional hardening should include:
-
Escape all other dynamic HTML fields, including recommendations, labels, summaries, and future metadata.
-
Prefer a template engine with automatic escaping rather than manual string concatenation.
-
Do not rely on input sanitization alone; perform output encoding for the exact destination context.
-
Add a restrictive Content Security Policy to generated reports, such as:
html <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; img-src data:"> -
Add regression tests containing
<script>elements, event-handler attributes, quotes, ampersands, malformed tags, and encoded payloads. -
Verify generated reports display malicious test strings literally and do not create executable DOM elements.
-
