T09 · Insecure Skill Coding Practices
- Location
scripts/md2html.py:99- Finding
Stored HTML and JavaScript Injection Through Unescaped Markdown Content
- Content
View full analysis
Vulnerability Details
File Location:
scripts/md2html.py, lines 99, 109, 127, 142, 154, and 159–166
Vulnerability Type: Stored HTML injection / cross-site scripting
Risk Level: HighVulnerable Code
python for cell in table_rows[0]: html_parts.append(f' <th>{render_inline(cell)}</th>')python for cell in row: html_parts.append(f' <td>{render_inline(cell)}</td>')python for item in list_items: html_parts.append(f' <li>{render_inline(item)}</li>')python html_parts.append(f' <p>{render_inline(" ".join(quote_lines))}</p>')python if para_text: html_parts.append(f'<p>{render_inline(para_text)}</p>')python def render_inline(text): """Render inline Markdown: bold, italic, code.""" text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text) text = re.sub(r'__(.+?)__', r'<strong>\1</strong>', text) text = re.sub(r'\*(.+?)\*', r'<em>\1</em>', text) text = re.sub(r'`([^`]+?)`', r'<code>\1</code>', text) return textTechnical Analysis
The
render_inline()function applies regular-expression substitutions directly to attacker-controlled Markdown but never HTML-escapes the input. Its return value is inserted into table cells, list items, blockquotes, and paragraphs as raw HTML.Unlike headings and fenced code blocks, which use
html_module.escape(), these rendering paths preserve arbitrary tags, event-handler attributes, SVG payloads, and other active browser content. Inline-code delimiters also provide no protection because characters inside backticks are wrapped in acodeelement without first being escaped.For example, the following Markdown remains an executable HTML element in the generated document:
markdown <img src=x onerror="alert(document.domain)">This is a stored injection issue: the malicious input is converted into a persistent
.htmlfil ...[truncated 1429 chars]- Remediation
View remediation
Remediation Suggestions
- Replace the custom regular-expression renderer with a maintained Markdown parser configured to escape or reject raw HTML.
- If the custom renderer is retained, HTML-escape all attacker-controlled text before generating formatting tags. Tokenize supported Markdown constructs, escape each text token with
html_module.escape(), and emit only internally generated allowlisted tags such asstrong,em, andcode. - Ensure escaping is consistently applied to paragraphs, tables, list items, blockquotes, and inline-code content.
- Do not attempt to fix the issue by escaping the final generated body, because that would also escape legitimate formatting tags. Use token-aware rendering or a proven sanitization library.
- Add regression tests for at least:
<script>elements<img onerror>handlers- SVG event handlers
- malformed and nested tags
- HTML inside tables, lists, blockquotes, and inline code
- Add a restrictive Content Security Policy to generated documents as defense in depth, while retaining contextual output escaping as the primary control.
