Back to skill

Security audit

md2img

Security checks for vulnerabilities and agentic risk

Overview

This Markdown-to-PNG skill is functional, but it can fetch remote image URLs, read local image paths from Markdown, and render them in an unsandboxed browser without enough user control.

Install only if you trust the Markdown inputs and the environment is contained. Prefer running it in a disposable virtualenv or container with restricted filesystem and network access, use --no-inline-images for untrusted Markdown, and avoid rendering documents that contain external URLs or local absolute image paths.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/md_to_png.py:143
Finding

Unrestricted Server-Side Request Forgery Through Markdown Image URLs

Content
View full analysis
tuple[bytes, str]: req = Request( url, headers={ # Some servers require UA. "User-Agent": "md2img/1.0", "Accept": "image/*,*/*;q=0.8", }, ) with urlopen(req, timeout=timeout_s) as resp: data = resp.read() content_type = resp.headers.get("Content-Type") if content_type: # e.g. "image/png; charset=binary" => keep only mime part mime = content_type.split(";", 1)[0].strip() if mime: return data, mime # Fallback by URL extension ext = Path(url.split("?", 1)[0].split("#", 1)[0]).suffix if ext: mime, _ = mimetypes.guess_type(ext) if mime: return data, mime return data, "application/octet-stream" def _inline_src(src: str) -> str | None: if src.startswith("data:"): return src try: if src.startswith("http://") or src.startswith("https://"): data, mime = _fetch_http(src) ``` ### Technical Analysis Image URLs extracted from attacker-controlled Markdown are passed directly to `urlopen`. The implementation does not restrict destination hosts, resolve and validate destination addresses, prohibit redirects, or reject loopback, private, link-local, multicast, and reserved IP ranges. The `Accept` header does not provide a security boundary. A target can return arbitrary content regardless of that header. Moreover, automatic redirects can move a request from an apparently acceptable public URL to a prohibited internal destination unless every redirect is independently validated. ### Attack Path 1. An attacker supplies Markdown containing an image reference ...[truncated 1019 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/md_to_png.py:132
Finding

Arbitrary Local File Access Through Markdown Image Paths

Content
View full analysis
Path | None: if src.startswith("/"): p = Path(src) else: p = (base_dir / src).resolve() return p def _inline_src(src: str) -> str | None: if src.startswith("data:"): return src try: if src.startswith("http://") or src.startswith("https://"): data, mime = _fetch_http(src) else: p = _resolve_local_path(src) if p is None or not p.exists() or not p.is_file(): return None data = p.read_bytes() mime = _guess_mime_from_path(p) b64 = base64.b64encode(data).decode("ascii") return f"data:{mime};base64,{b64}" ``` ### Technical Analysis The image source is controlled by Markdown input. Absolute filesystem paths are accepted directly, and relative paths are resolved without checking that the resulting path remains under the Markdown file's directory or another approved asset root. Consequently, traversal sequences such as `../../...` can escape the intended input directory. Any file readable by the converter process can be loaded and converted into a data URI. Files that Chromium can render as images, particularly local SVG or image files, may become visible in the resulting PNG. The implementation also relies on filename-based MIME inference rather than validating file signatures and content. ### Attack Path 1. An attacker supplies Markdown containing an image source with an absolute path or a traversing relative path, such as ``. 2. The Markdown is converted to HTML, and the permitted `img` element and `src` attribute survive sanitization. 3. `_resolve_local_path` resolves the source without enforcing directory ...[truncated 726 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/md_to_png.py:284
Finding

Headless Chromium Sandbox Explicitly Disabled for Untrusted Rendering

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:48
Finding

Unpinned Third-Party Packages and Browser Components

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/md_to_png.py:143
Finding

Unbounded Resource Reads Permit Memory and Bandwidth Exhaustion

Content
View full analysis
tuple[bytes, str]: req = Request( url, headers={ # Some servers require UA. "User-Agent": "md2img/1.0", "Accept": "image/*,*/*;q=0.8", }, ) with urlopen(req, timeout=timeout_s) as resp: data = resp.read() content_type = resp.headers.get("Content-Type") if content_type: # e.g. "image/png; charset=binary" => keep only mime part mime = content_type.split(";", 1)[0].strip() if mime: return data, mime # Fallback by URL extension ext = Path(url.split("?", 1)[0].split("#", 1)[0]).suffix if ext: mime, _ = mimetypes.guess_type(ext) if mime: return data, mime return data, "application/octet-stream" def _inline_src(src: str) -> str | None: if src.startswith("data:"): return src try: if src.startswith("http://") or src.startswith("https://"): data, mime = _fetch_http(src) else: p = _resolve_local_path(src) if p is None or not p.exists() or not p.is_file(): return None data = p.read_bytes() mime = _guess_mime_from_path(p) b64 = base64.b64encode(data).decode("ascii") return f"data:{mime};base64,{b64}" ``` ### Technical Analysis Remote responses are consumed with an unbounded `resp.read()`, while local resources are consumed with an unbounded `Path.read_bytes()`. Each resource is then Base64-encoded, increasing its in-memory representation by approximately one third before it is copied into the HTML document and processed again by Chromium. The network timeout limits waiting time but does not cap b ...[truncated 1056 chars]
Remediation
View remediation
Vulnerability Patterns
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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
Findings (6)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The documented purpose says the conversion is local and supports inline Markdown or a .md path, but the detected behavior includes outbound HTTP(S) fetches for remote images and reading additional local image files referenced from Markdown. This can leak network metadata, enable SSRF-style requests from the agent environment, and exfiltrate or expose local files beyond the user-declared input, especially because Markdown content can reference attacker-controlled URLs or sensitive local paths.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill declares no tool scope or permissions boundaries, yet its documented behavior includes reading user-supplied .md paths and using a headless browser that can fetch external resources. That mismatch matters because consumers may assume a purely local conversion tool, while the skill can touch the filesystem and trigger network access implicitly through embedded images or browser loading behavior.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The skill content switches to Chinese and states its objective entirely in Chinese, which effectively imposes a specific language/locale in the skill instructions without indicating user choice or opt-in. This matches the policy category for language or locale constraints that are not optional or justified as region-specific.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill is presented as a local Markdown-to-PNG converter, but it will fetch remote HTTP/HTTPS images referenced in Markdown and embed them into the rendered document. This creates undeclared outbound network access and can leak environment/network metadata, contact attacker-controlled hosts, or be used for SSRF-style access attempts when untrusted Markdown is rendered.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The code path that resolves image sources performs arbitrary HTTP/HTTPS requests during conversion even though the skill's purpose is nominally local rendering. In the context of agent execution on user- or model-supplied Markdown, this is dangerous because embedded image URLs can trigger unexpected external requests, tracking beacons, or access to internal services reachable from the runtime.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The generated HTML hard-codes lang="zh-CN", which imposes a specific language/locale policy on all rendered output regardless of the user's content or preference. The file also contains Chinese-only dependency error messages, reinforcing a fixed-language experience without opt-in or documented regional scope.

Content

No source excerpt is available for this finding.