Back to skill

Security audit

md-to-html

Security checks for vulnerabilities and agentic risk

Overview

The skill does what it claims, but its HTML converter can carry unsafe raw HTML or JavaScript from a Markdown file into the generated page.

Install only if you will convert Markdown you trust or can review first. Do not open, host, or share generated HTML from untrusted Markdown unless the converter is fixed to escape or sanitize raw HTML and inline-code contents.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Error
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: High

Vulnerable 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 text

Technical 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 a code element 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 .html fil ...[truncated 1429 chars]

Remediation
View remediation

Remediation Suggestions

  1. Replace the custom regular-expression renderer with a maintained Markdown parser configured to escape or reject raw HTML.
  2. 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 as strong, em, and code.
  3. Ensure escaping is consistently applied to paragraphs, tables, list items, blockquotes, and inline-code content.
  4. 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.
  5. 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
  6. Add a restrictive Content Security Policy to generated documents as defense in depth, while retaining contextual output escaping as the primary control.
Vulnerability Patterns
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (4)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
86% confidence
Finding

The skill instructs execution of a bundled Python script and implies reading a user-supplied Markdown file and writing an HTML file, but it declares no explicit tool scope such as file read/write permissions. That mismatch can lead to over-broad agent behavior or unclear authorization boundaries, increasing the chance the agent accesses or writes files outside the intended workspace when handling attacker-influenced paths.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The trigger phrases are broad enough that the skill may activate on generic requests about converting Markdown to HTML without sufficient user confirmation or context. In an agent environment, over-broad activation can cause unintended file processing or writing outputs when the user did not explicitly select this skill or specify the target file.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The inline renderer injects user-controlled Markdown text into HTML tags using regex replacements without HTML-escaping the original text first. As a result, raw HTML such as

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
96% confidence
Finding

The generated HTML always sets lang="zh-CN", which imposes a specific locale regardless of the source markdown language or user preference. This is a natural-language locale policy concern because the file does not offer opt-in, configuration, or documentation justifying the restriction.

Content

No source excerpt is available for this finding.