Back to skill

Security audit

Markdown to Page

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it claims, but its Markdown-to-HTML converter can preserve unsafe HTML and can embed local files outside the document folder when image embedding is enabled.

Use this only with Markdown files you trust, especially if you will publish or share the generated HTML. Avoid --embed-images on untrusted documents because crafted image paths may package local files into the output. Treat generated HTML as active web content, not a sanitized document.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/md_to_page.py:25
Finding
Stored HTML and JavaScript Injection in Generated Pages<![CDATA[ ## Vulnerability Details **File Location**: `scripts/md_to_page.py`, lines 25–26, 100–121, 646–676, and 1074–1075 **Vulnerability Type**: Stored HTML injection and cross-site scripting in generated HTML **Risk Level**: High ### Vulnerable Code ```python def escape(text: str) -> str: return html_module.escape(text, quote=False) ``` ```python def inline_md(text: str) -> str: """Convert inline markdown: bold, italic, code, links, images.""" # Images text = re.sub( r'!\[([^\]]*)\]\(([^)]+)\)', lambda m: f'<img src="{escape(m.group(2))}" alt="{escape(m.group(1))}" style="max-width:100%;border-radius:8px;margin:0.5rem 0">', text, ) # Links text = re.sub( r'\[([^\]]+)\]\(([^)]+)\)', lambda m: f'<a href="{escape(m.group(2))}" target="_blank" style="color:var(--accent);text-decoration:underline">{m.group(1)}</a>', text, ) # Inline code text = re.sub( r'`([^`]+)`', lambda m: f'<code style="background:var(--bg-code);padding:0.15em 0.4em;border-radius:4px;font-size:0.9em">{escape(m.group(1))}</code>', text, ) # Bold text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text) # Italic (single *) text = re.sub(r'(?<!\*)\*([^*]+?)\*(?!\*)', r'<em>\1</em>', text) return text ``` Representative rendering sinks include: ```python elif btype == 'paragraph': text = inline_md(block[1]) content_html.append(f' <p class="reveal">{text}</p>') elif btype == 'blockquote': text = block[1] if text.strip().startswith('💡'): cls = 'callout insight reveal' text = text.replace('💡', '', 1).strip() elif text.strip().startswith('⚠️'): cls = 'callout warn reveal' text = text.replace('⚠️', '', 1).strip() else: cls = 'callout reveal' content_html.append(f' <div class="{cls}">{inline_md(text)}</div>') elif btype == 'ul': items_html = '\n'.join(f' <li>{inline_md(item)}</li ...[truncated 3170 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use a maintained Markdown rendering library with explicit safe-mode or HTML-sanitization support rather than constructing HTML with regular expressions. 2. Disable raw HTML in Markdown by default. If raw HTML is required, sanitize the rendered result with a strict allowlist of elements and attributes. 3. Escape all source text before applying controlled Markdown transformations. 4. Change context-sensitive escaping to encode quotation marks: ```python html_module.escape(value, quote=True) ``` 5. Escape link labels, directive attributes, headings, and all other user-controlled text before inserting them into HTML. 6. Parse URLs and permit only required schemes: - Links: normally `http`, `https`, and optionally `mailto`. - Images: normally `http`, `https`, and explicitly generated image `data:` URIs. - Reject `javascript:`, `vbscript:`, and unexpected `data:` content. 7. Add `rel="noopener noreferrer"` to links opened with `target="_blank"`. 8. Add a restrictive Content Security Policy to generated pages. Avoid allowing inline scripts or inline event handlers where possible. 9. Add regression tests containing raw `<script>` tags, event-handler attributes, quote-breaking URLs, encoded `javascript:` URLs, malformed Markdown, and nested directive content. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/md_to_page.py:130
Finding
Unrestricted Local File Access Through Image Path Traversal<![CDATA[ ## Vulnerability Details **File Location**: `scripts/md_to_page.py`, lines 130–173 and 1141–1144 **Vulnerability Type**: Path traversal and unintended local-file embedding **Risk Level**: Medium ### Vulnerable Code ```python def embed_images_in_md(md_text: str, base_dir: Path) -> str: """Replace local image references with base64 data URIs.""" def replace_img(m): alt = m.group(1) src = m.group(2) # Skip URLs if src.startswith('http://') or src.startswith('https://'): return m.group(0) if src == 'placeholder' or src.startswith('placeholder'): return m.group(0) img_path = base_dir / src if not img_path.exists(): return m.group(0) try: # Try Pillow compression from PIL import Image as PILImage import io img = PILImage.open(img_path) # Convert RGBA to RGB for JPEG if img.mode in ('RGBA', 'P'): img = img.convert('RGB') # Resize if wider than 1200px if img.width > 1200: ratio = 1200 / img.width img = img.resize((1200, int(img.height * ratio)), PILImage.LANCZOS) buf = io.BytesIO() img.save(buf, format='JPEG', quality=75, optimize=True) b64 = base64.b64encode(buf.getvalue()).decode() data_uri = f'data:image/jpeg;base64,{b64}' except ImportError: # Fallback: raw base64 raw = img_path.read_bytes() ext = img_path.suffix.lower() mime_map = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml'} mime = mime_map.get(ext, 'image/png') b64 = base64.b64encode(raw).decode() data_uri = f'data:{mime};base64,{b64}' return f'![{alt}]({data_uri})' return re.sub(r'!\[([^ ...[truncated 3780 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the approved base directory and candidate path before access: ```python root = base_dir.resolve(strict=True) candidate = (root / src).resolve(strict=True) ``` 2. Reject absolute Markdown image paths before joining them to the root. 3. Enforce containment using `Path.is_relative_to()` on supported Python versions: ```python if not candidate.is_relative_to(root): return m.group(0) ``` For older versions, use a robust `os.path.commonpath()` comparison rather than string-prefix checks. 4. Reject paths containing traversal outside the approved root, including paths reached through symbolic links. 5. Require `candidate.is_file()` and reject devices, pipes, sockets, directories, and other special files. 6. Verify content using an image decoder rather than trusting the filename extension. Call Pillow verification before processing the image. 7. Remove the unrestricted raw-byte fallback. If Pillow is unavailable, either leave the reference unchanged or embed only files whose type has been securely validated. 8. Apply maximum input-file dimensions and byte-size limits before reading or decoding. 9. Consider requiring an explicit image root or allowlist rather than implicitly granting access to every path readable by the current user. 10. Warn users that `--embed-images` packages referenced local content into a portable output file. 11. Add tests for `../` traversal, absolute paths, symlink escapes, nonexistent paths, special files, oversized files, and mislabeled non-image files. ]]>
Vulnerability Patterns
  • Memory PoisoningPersistent Context Injection, Context Window Stuffing, Memory Manipulation
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • 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)

Memory Manipulation

High
Category
Memory Poisoning
Content
::: cmd-list
/status — Check agent status
/model — Switch model
/new — Reset context
:::
```
Confidence
80% confidence
Finding
Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

Intent-Code Divergence

Medium
Confidence
99% confidence
Finding
The inline markdown renderer escapes link URLs but inserts link text with `m.group(1)` directly into HTML, and later formatting substitutions for bold/italic also operate on unescaped text. An attacker who controls Markdown input can inject arbitrary HTML/JavaScript into the generated page, leading to stored XSS when the output HTML is opened in a browser.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The generated page hard-codes `lang="zh-CN"`, which imposes a specific language/locale on all output regardless of the user's content or preferences. This is a natural-language policy concern because the file does not offer any locale choice or document a justified region-specific constraint.

Tainted flow: 'html_output' from pathlib.Path.read_text (line 1147, file read) → pathlib.Path.write_text (file write)

Medium
Category
Data Flow
Content
output_path = Path(args.output)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    output_path.write_text(html_output, encoding='utf-8')
    print(f"✅ Generated: {output_path}")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Static analysis

No suspicious patterns detected.