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

Content
View full analysis
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'{escape(m.group(1))}', text, ) # Links text = re.sub( r'\[([^\]]+)\]\(([^)]+)\)', lambda m: f'{m.group(1)}', text, ) # Inline code text = re.sub( r'`([^`]+)`', lambda m: f'{escape(m.group(1))}', text, ) # Bold text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # Italic (single *) text = re.sub(r'(?\1', text) return text ``` Representative rendering sinks include: ```python elif btype == 'paragraph': text = inline_md(block[1]) content_html.append(f'

{text}

') 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'
{inline_md(text)}
') elif btype == 'ul': items_html = '\n'.join(f'
  • {inline_md(item)}
  • Remediation
    View remediation
    ` 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

    Content
    View full analysis
    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
    View remediation
    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
    Confidence
    80% confidence
    Finding

    Skill manipulates agent memory, state, or stored context. Memory corruption can alter personality, override safety rules, or cause unpredictable behavior.

    Content

    Scanner excerpt · SKILL.md (reported line 149)May include surrounding context.

    ::: cmd-list /status — Check agent status /model — Switch model /new — Reset context :::

    text

    Intent-Code Divergence

    Medium
    Category
    Not specified by scanner
    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.

    Content

    No source excerpt is available for this finding.

    Natural-Language Policy Violations

    Medium
    Category
    Not specified by scanner
    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.

    Content

    No source excerpt is available for this finding.

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

    Medium
    Category
    Data Flow
    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.

    Content

    Scanner excerpt · scripts/md_to_page.py (reported line 1151)May include surrounding context.

    python
    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}")