Back to skill

Security audit

Markdown Mobile Export

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does the advertised Markdown-to-image export, but it can automatically install packages and a browser and can render active untrusted HTML in Chromium.

Install only if you are comfortable with the skill modifying the Python environment, downloading browser components when needed, and rendering Markdown in a browser. Prefer using it in an isolated environment with dependencies preinstalled, and avoid processing Markdown from untrusted sources unless raw HTML and remote network requests are disabled or sandboxed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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)

T08 · Insecure Dependencies

Warning
Location
scripts/export_long_image.py:14
Finding
Automatic Installation of Unpinned Runtime Dependencies<![CDATA[ ## Vulnerability Details **File Location**: `scripts/export_long_image.py:14-31, 88-114`; `scripts/render_markdown_mobile_long_image.py:631-664` **Vulnerability Type**: Uncontrolled installation of mutable third-party dependencies **Risk Level**: Medium ### Vulnerable Code From `scripts/export_long_image.py`: ```python def _run_install(package_name: str) -> None: ensure_commands = [ [sys.executable, "-m", "ensurepip", "--upgrade"], ] install_commands = [ [sys.executable, "-m", "pip", "install", package_name], ] uv_path = shutil.which("uv") if uv_path: install_commands.append([uv_path, "pip", "install", "--python", sys.executable, package_name]) for ensure_command in ensure_commands: subprocess.run(ensure_command, check=False) last_error: Exception | None = None for command in install_commands: try: subprocess.run(command, check=True) return except Exception as exc: # noqa: BLE001 last_error = exc if last_error is not None: raise last_error ``` ```python def ensure_playwright_module() -> None: try: importlib.import_module("playwright.sync_api") return except ImportError: pass _run_install("playwright") importlib.import_module("playwright.sync_api") def install_playwright_browser() -> None: subprocess.run( [sys.executable, "-m", "playwright", "install", "chromium"], check=True, ) def ensure_pillow() -> None: try: importlib.import_module("PIL.Image") return except ImportError: pass _run_install("pillow") importlib.import_module("PIL.Image") ``` The renderer contains an equivalent installer in `scripts/render_markdown_mobile_long_image.py`: ```python def _run_install(package_name: str) -> None: ensure_commands = [ [sys.executable, "-m", "ensurepip", "--upgrade"], ] install_commands = [ ...[truncated 3013 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove automatic dependency installation from the document-conversion path. 2. Declare exact dependency versions in a project manifest and generate a lockfile. 3. Require hashes for downloaded distributions, for example through a hash-locked requirements file. 4. Install dependencies during a separate, explicit setup phase rather than during content processing. 5. Use an isolated virtual environment or container with only the required packages. 6. Restrict installation to an explicitly trusted package index and prevent inherited alternate-index configuration where feasible. 7. Pin the Playwright browser build and provision it during installation or image construction. 8. If a dependency is unavailable at runtime, fail safely with installation instructions rather than modifying the environment. 9. Run dependency scanning and signature or provenance verification in the release pipeline. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/render_markdown_mobile_long_image.py:667
Finding
Untrusted Markdown Can Execute Active HTML and Initiate Browser Requests<![CDATA[ ## Vulnerability Details **File Location**: `scripts/render_markdown_mobile_long_image.py:610-627, 667-673, 766-787`; `scripts/export_long_image.py:224-238, 292-299` **Vulnerability Type**: Unsanitized active HTML rendered in a network-enabled browser **Risk Level**: Medium ### Vulnerable Code The generated document inserts rendered Markdown directly into the HTML body: ```python HTML_TEMPLATE = """<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>{title}</title> <style>{css}</style> </head> <body> <article class="page"> <div class="top-band"></div> <div class="page-inner"> {content} </div> <div class="bottom-band"></div> </article> </body> </html> """ ``` Raw HTML is explicitly enabled: ```python def build_markdown_renderer() -> MarkdownIt: ensure_markdown_it() from markdown_it import MarkdownIt return ( MarkdownIt("commonmark", {"html": True, "linkify": True, "breaks": True}) .enable("table") .enable("strikethrough") ) ``` The rendered content and inferred title are inserted without an HTML sanitizer. The inferred title is also not escaped: ```python def render_markdown_text(markdown_text: str, title_hint: str = "") -> str: renderer = build_markdown_renderer() rendered = renderer.render(markdown_text) title = infer_title(markdown_text, title_hint=title_hint) content_html = enhance_rendered_html(rendered) return HTML_TEMPLATE.format(title=title, css=CSS, content=content_html) def infer_title(markdown_text: str, title_hint: str = "") -> str: for line in markdown_text.splitlines(): stripped = line.strip() if stripped.startswith("# "): return stripped[2:].strip() return title_hint or "Markdown Long Image" ``` The resulting local document is loaded in a browser without request interception or network isolation: ```python ...[truncated 4003 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable raw HTML by default: ```python MarkdownIt("commonmark", {"html": False, "linkify": True, "breaks": True}) ``` 2. If limited HTML support is required, sanitize the rendered fragment using a strict allowlist before inserting it into the template. 3. Remove scripts, iframes, object/embed elements, active SVG content, form controls, event-handler attributes, and dangerous URL schemes. 4. Escape the inferred title before template insertion: ```python title = escape(infer_title(markdown_text, title_hint=title_hint), quote=True) ``` 5. Add Playwright request interception and deny all network requests by default. 6. If remote images are required, expose them through an explicit opt-in option and allow only `https` image requests to approved hosts. 7. Block requests to loopback, link-local, private, metadata-service, and internal network address ranges. 8. Apply a restrictive Content Security Policy that disables scripts, frames, plugins, forms, and unexpected connections. 9. Consider downloading approved images through a hardened fetcher with size limits, content-type validation, redirect limits, and SSRF protections, then render them from controlled local files. 10. Run Chromium with current security patches inside a low-privilege container or sandbox with restricted filesystem and network access. 11. Add tests containing script tags, event attributes, hostile SVG, malformed title markup, `javascript:` URLs, remote resources, and internal-network URLs. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (19)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The documented behavior does not match the detected implementation behavior, including undeclared package installation and subprocess execution. Behavior mismatch is security-relevant because operators may approve a seemingly simple markdown export skill while it actually performs additional execution and environment modification that can expand attack surface or enable supply-chain abuse.

Context-Inappropriate Capability

High
Confidence
97% confidence
Finding
The script can install Python packages at runtime using pip and uv, a powerful capability not necessary for the core task and not clearly disclosed. In this skill context, that makes the behavior more dangerous because users expect local content conversion, not arbitrary dependency bootstrap with network retrieval and execution of third-party package code.

Context-Inappropriate Capability

High
Confidence
95% confidence
Finding
Installing a browser binary at runtime materially expands the skill's operational footprint and trust boundary. For a local export tool, downloading executable browser components without prior disclosure creates unnecessary supply-chain, persistence, and policy-compliance risks.

Context-Inappropriate Capability

High
Confidence
94% confidence
Finding
The script silently installs Python packages at runtime by invoking pip/uv, which introduces uncontrolled code execution from external package sources during a content-rendering task. In the context of a local Markdown conversion skill, this is unnecessarily dangerous because execution now depends on network, package index trust, environment state, and PATH resolution, expanding the attack surface well beyond rendering user content.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill advertises operational behavior that implies file read, file write, and shell execution, but it does not declare any explicit tool scope or permissions boundary. This increases the chance an agent will invoke it with broader-than-expected capabilities, making unintended filesystem access or command execution harder to review and constrain.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The skill retains generated HTML sidecar files and normalized markdown on disk without a clear warning to the user. If the markdown contains sensitive notes, reports, credentials, or proprietary content, those residual files can persist in locations users do not expect and may later be exposed through local access, backups, or other processes.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
Automatic pip/uv installation without user-facing warning is unsafe because it performs networked environment changes under the guise of a simple export action. The lack of confirmation or disclosure prevents informed consent and can violate least-privilege expectations for local content-processing tools.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
install_commands.append([uv_path, "pip", "install", "--python", sys.executable, package_name])

    for ensure_command in ensure_commands:
        subprocess.run(ensure_command, check=False)

    last_error: Exception | None = None
    for command in install_commands:
Confidence
88% confidence
Finding
This subprocess call invokes ensurepip automatically, altering the Python environment without prompting the user. In the context of a local Markdown/image export skill, unexpected package-management behavior is dangerous because it expands execution beyond file conversion into environment mutation and can enable downstream unreviewed package installation.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
last_error: Exception | None = None
    for command in install_commands:
        try:
            subprocess.run(command, check=True)
            return
        except Exception as exc:  # noqa: BLE001
            last_error = exc
Confidence
91% confidence
Finding
This subprocess call performs automatic package installation via pip or uv during normal execution. Even without command injection, runtime installation introduces supply-chain risk, network access, and unanticipated system changes that are not justified by the stated purpose of a simple export utility.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
Silently installing Playwright browser binaries creates hidden network and software installation side effects. In a skill advertised as a local Markdown export helper, undisclosed installation behavior is especially risky because it exceeds user expectations and broadens the executable surface.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def install_playwright_browser() -> None:
    subprocess.run(
        [sys.executable, "-m", "playwright", "install", "chromium"],
        check=True,
    )
Confidence
90% confidence
Finding
This subprocess invocation installs a Playwright-managed Chromium browser at runtime, which causes unexpected network access and modification of the local environment during a document export task. While the command is not shell-injected, silently downloading and installing executable browser binaries expands the skill's capabilities and increases supply-chain and trust risks.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The manifest describes converting a local Markdown path or pasted Markdown text into a mobile-friendly image while keeping HTML beside it for inspection. This file does not process Markdown at all; it consumes an HTML file and, if needed, installs Playwright and a Chromium browser before rendering and capturing screenshots, which is materially broader than the stated conversion behavior.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
Automatically performing package installation without any user-facing warning or confirmation is unsafe because it triggers privileged environmental changes and remote code retrieval as a side effect of a simple rendering operation. In a skill expected to process local Markdown, this hidden behavior is especially risky and surprising, making compromise or policy violations more likely in restricted or production environments.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
install_commands.append([uv_path, "pip", "install", "--python", sys.executable, package_name])

    for ensure_command in ensure_commands:
        subprocess.run(ensure_command, check=False)

    last_error: Exception | None = None
    for command in install_commands:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
last_error: Exception | None = None
    for command in install_commands:
        try:
            subprocess.run(command, check=True)
            return
        except Exception as exc:  # noqa: BLE001
            last_error = exc
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The manifest describes converting Markdown into a mobile-friendly PNG/JPG long image while keeping HTML beside it, but this file's CLI only accepts an input Markdown path and an output HTML path, then writes HTML. No image rendering, screenshotting, or PNG/JPG generation occurs anywhere in the code, so the implemented behavior is materially narrower than the advertised skill behavior.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The natural-language description sets `zh-CN typography` as the default style, which imposes a specific locale preference in the skill behavior. The file does not indicate user opt-in, configurability, or a region-specific justification for that locale choice.

Missing User Warnings

Low
Confidence
77% confidence
Finding
This markdown file describes that the entry script produces a normalized markdown path, rendered HTML file, and exported image file, which means it writes files to disk. The workflow does not include any warning or disclosure about these local artifacts being created or overwritten, which is relevant user-impacting behavior for a markdown skill description.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The generated HTML always sets lang="zh-CN", which forces a specific language/locale regardless of the input content or user preference. This is a natural-language locale policy concern because the file does not provide opt-in, configurability, or justification for restricting output to Chinese locale metadata.

Static analysis

No suspicious patterns detected.