Back to skill

Security audit

picture-book-maker

Security checks for vulnerabilities and agentic risk

Overview

The skill is a coherent picture-book workflow, but its packer can be abused through metadata to read local files, contact arbitrary URLs, or inject active content into generated books.

Install only if you trust the book metadata and image sources you will process. Avoid running the packer on untrusted metadata.json files, do not allow arbitrary remote image URLs, and treat generated book.html files as active web content until the script adds path containment, URL restrictions, strict image validation, and HTML escaping.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pack-book.py:94
Finding
Arbitrary Local File Read and Base64 Disclosure Through Metadata Image Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pack-book.py:94-109, 201-206` **Vulnerability Type**: Path traversal and unrestricted local file read **Risk Level**: High ### Vulnerable Code ```python else: # Read a local file if not os.path.exists(image_path_or_url): raise Exception(f"Image file does not exist: {image_path_or_url}") with open(image_path_or_url, 'rb') as f: image_data = f.read() img_format = image_path_or_url.split('.')[-1].lower() # Validate image data size if len(image_data) < 1000: raise Exception( f"Abnormal image data (only {len(image_data)} bytes): " f"{image_path_or_url}" ) # Convert to Base64 image_base64 = base64.b64encode(image_data).decode('utf-8') ``` The path passed to this function is assembled as follows: ```python if image_source.startswith('http://') or image_source.startswith('https://'): # URL image full_path = image_source else: # Local file full_path = os.path.join(pages_dir, image_source) image_url = image_to_base64(full_path, max_retries) ``` ### Technical Analysis When the local `pages/` directory does not contain image files, image source values are read from attacker-controllable fields in `metadata.json`. A non-HTTP value is treated as a local filename and joined to `pages_dir`. The implementation does not reject: - Absolute paths - `../` path traversal components - Symbolic links that resolve outside `pages_dir` - Files that are not actually images With `os.path.join`, an absolute `image_source` can replace the intended base directory entirely. Relative traversal sequences can also escape from `pages_dir`. The resulting path is passed to `open()` and read with the process's filesystem privileges. Local file content is not passed through `is_valid_image()`. Apart from the minimum size check, arbitrary local data can therefore be Base64-encoded and embedded in the generated HTML as a data URI. The Base64 operation itsel ...[truncated 1582 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject absolute paths from metadata. 2. Canonicalize both the page directory and requested file: ```python base_dir = os.path.realpath(pages_dir) candidate = os.path.realpath(os.path.join(base_dir, image_source)) if os.path.commonpath([base_dir, candidate]) != base_dir: raise ValueError("Image path escapes the pages directory") ``` 3. Reject traversal components before path resolution as defense in depth. 4. Ensure the resolved path is a regular file and handle symbolic links safely. 5. Apply image magic-byte validation to local files as well as downloaded files. 6. Use a strict allowlist of supported image formats rather than accepting unknown binary content. 7. Enforce reasonable maximum input-file sizes to prevent memory exhaustion. 8. Run the packer with a dedicated, low-privilege account that cannot read unrelated secrets. 9. Add tests covering absolute paths, `../` traversal, nested traversal, symbolic links, and non-image files. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pack-book.py:58
Finding
Server-Side Request Forgery Through Unrestricted Remote Image URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pack-book.py:58-74, 81-86, 201-203` **Vulnerability Type**: Server-side request forgery **Risk Level**: High ### Vulnerable Code ```python def download_image(url, max_retries=3): """Download an image with retries.""" for attempt in range(max_retries): try: response = requests.get(url, timeout=30) response.raise_for_status() return response.content except Exception as e: print( f"Image download failed " f"(attempt {attempt + 1}/{max_retries}): {url}" ) print(f"Error: {str(e)}") if attempt < max_retries - 1: wait_time = (attempt + 1) * 2 print(f"Waiting {wait_time} seconds before retry...") time.sleep(wait_time) else: return None return None ``` Remote metadata values are sent to that function without destination validation: ```python if image_path_or_url.startswith('http://') or image_path_or_url.startswith('https://'): image_data = download_image(image_path_or_url, max_retries) if image_data is None: raise Exception(f"Unable to download image: {image_path_or_url}") ``` The source selection similarly trusts the metadata URL: ```python if image_source.startswith('http://') or image_source.startswith('https://'): full_path = image_source ``` ### Technical Analysis The packer permits arbitrary HTTP and HTTPS image URLs from `metadata.json` and executes requests from the machine running the Skill. It does not enforce: - A trusted-host allowlist - HTTPS-only transport - Restrictions on loopback, private, link-local, multicast, or reserved addresses - DNS rebinding protections - Redirect destination validation - A maximum response size - Strict response `Content-Type` checking - Strict image-format validation `requests.get()` follows redirects by default. Even i ...[truncated 1884 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer local-only image inputs and disable arbitrary remote fetching unless it is strictly required. 2. If remote images are required, use an explicit allowlist of trusted HTTPS hosts. 3. Resolve the hostname before connecting and reject all loopback, private, link-local, multicast, unspecified, and reserved IP ranges for both IPv4 and IPv6. 4. Revalidate the destination after every DNS resolution and redirect. 5. Disable redirects by default or allow only a small number of redirects whose destinations independently pass validation. 6. Reject URLs containing credentials or unexpected ports. 7. Stream responses instead of loading them entirely into memory. 8. Enforce a strict maximum response size. 9. Require an approved image `Content-Type` and verify it against strict magic-byte parsing. 10. Change `is_valid_image()` so unknown content returns `False`. 11. Consider processing remote images through a controlled image proxy with network isolation. 12. Apply outbound firewall rules so the Skill cannot reach local, metadata, or private network ranges. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pack-book.py:234
Finding
Stored HTML and JavaScript Injection in Generated Books<![CDATA[ ## Vulnerability Details **File Location**: `scripts/pack-book.py:234-243, 256-260, 267-287` **Vulnerability Type**: Stored HTML injection and cross-site scripting **Risk Level**: High ### Vulnerable Code Page text from metadata is converted into HTML without escaping: ```python if 'text' in page_data: page_text = page_data['text'] elif 'text_cn' in page_data or 'text_en' in page_data: text_cn = page_data.get('text_cn', '') text_en = page_data.get('text_en', '') if text_cn and text_en: page_text = ( f'<div class="text-cn">{text_cn}</div>' f'<div class="text-en">{text_en}</div>' ) elif text_cn: page_text = f'<div class="text-cn">{text_cn}</div>' elif text_en: page_text = f'<div class="text-en">{text_en}</div>' ``` Cover metadata is also interpolated directly: ```python cover_html = f''' <div class="sheet sheet-cover" id="sheet-cover"> <div class="cover-content"> <h1 class="cover-title">{metadata.get('title', '绘本')}</h1> <div class="cover-author">作者:{metadata.get('author', '未知')}</div> {f'<div class="cover-description">{metadata.get("description", "")}</div>' if metadata.get('description') else ''} <button class="cover-btn" id="startBtn">开始阅读</button> </div> </div>''' ``` The resulting untrusted values are inserted into the final document: ```python <div class="page-text">{page['text']}</div> ``` The title is inserted into the document head without contextual escaping: ```python <title>{metadata.get('title', '绘本')}</title> ``` ### Technical Analysis The script treats metadata fields as trusted HTML. Values including `title`, `author`, `description`, `text`, `text_cn`, and `text_en` are inserted directly into the generated document without HTML escaping or sanitization. An attacker can close the surrounding element and inject arbitrary HTML. Depending on browser behavior and the injection context, payloads may include: - Ele ...[truncated 1948 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Treat all metadata fields as plain text by default. 2. Escape text with contextual HTML encoding before interpolation: ```python import html safe_title = html.escape(str(metadata.get('title', 'Picture Book')), quote=True) safe_author = html.escape(str(metadata.get('author', 'Unknown')), quote=True) safe_description = html.escape( str(metadata.get('description', '')), quote=True ) safe_text_cn = html.escape(str(text_cn), quote=True) safe_text_en = html.escape(str(text_en), quote=True) ``` 3. Do not allow the generic `text` field to contain unrestricted HTML. 4. If formatting is required, sanitize it with a mature allowlist-based HTML sanitizer and permit only non-active formatting elements. 5. Remove scripts, event-handler attributes, unsafe URL schemes, forms, embedded objects, and active SVG content. 6. Apply context-specific escaping for HTML text, attributes, URLs, CSS, and JavaScript rather than using one encoding method everywhere. 7. Add a restrictive Content Security Policy, preferably disallowing inline scripts and external network connections. 8. Move the book navigation JavaScript to a trusted external file or use a nonce/hash-based CSP if inline script is unavoidable. 9. Add regression tests containing closing tags, event handlers, SVG payloads, encoded payloads, and malformed markup. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
Findings (10)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The skill is presented as an end-to-end picture-book creation workflow, but the documented operational behavior materially differs: the only concrete implementation implied is packaging/rendering and optional remote image downloading. This mismatch can mislead users and calling systems about what the skill actually does, obscuring network access and file-processing behavior that carries security and privacy implications.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill clearly describes capabilities that read local reference files, write output files, and fetch remote images, yet it declares no explicit tool scope or permission boundaries. This weakens least-privilege controls and increases the chance the agent can perform unintended file or network actions when the skill is invoked.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger conditions are broad enough to match many ordinary creative-writing or illustration requests, which raises the risk of unintended invocation. In context, unintended invocation matters because this skill can create local files and later trigger packaging or network retrieval steps that the user may not have intended.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill instructs the agent to save files and generate packaged output but does not clearly warn that it will create and modify local directories and files. Lack of transparency around filesystem side effects can lead to unauthorized or surprising local changes, especially if the skill is triggered automatically or by vague prompts.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The packaging flow allows downloading remote image URLs without clearly warning about network activity, privacy exposure, or the trust boundary introduced by external content. This can leak environment metadata to third-party hosts and exposes the system to risks from untrusted remote resources, especially when URL inputs come from metadata.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The format requirements state that 'text_cn' is required while 'text_en' is optional, effectively forcing Chinese as the primary content language. This is a language-policy constraint presented without user opt-in, alternatives, or justification that the skill is region-specific.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
This markdown file is entirely presented in Chinese, starting with the title and continuing throughout the template, with no indication that language selection is optional. Under the stated policy, forcing a specific language without user opt-in is a natural-language policy violation unless the locale constraint is explicitly documented and justified.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The generated document hard-codes `lang="zh-CN"`, which imposes a specific language/locale on all output. The file does not provide any user opt-in, configuration option, or documented region-specific justification for this constraint.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
L021 写明“本技能使用智能体原生能力,无需安装额外依赖包”,但文件头部 L004-L006 明确列出了 `requests>=2.28.0` 作为 Python 依赖。这属于文档与实际技能定义直接矛盾,而不是简单的信息缺失。

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
This markdown file is entirely written in Chinese and presents the guidance as the default format, but it does not indicate that Chinese is optional or limited to a Chinese-language context. Under the policy rule for language or locale constraints, this can be considered a natural-language policy issue because it implicitly forces one language without user opt-in.

Static analysis

No suspicious patterns detected.