Back to skill

Security audit

aws-wechat-article-formatting

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent local WeChat Markdown-to-HTML formatter, but it needs review because untrusted inputs can produce active HTML and theme paths are not tightly contained.

Install only if you will format trusted local drafts and trusted theme/component presets. Do not run it on Markdown, article.yaml, themes, or components from untrusted contributors, and be careful opening generated HTML previews in a browser. The skill does not show exfiltration or persistence, but it should add path containment and HTML sanitization before being treated as safe for untrusted content.

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/format.py:1649
Finding
Unsanitized Markdown permits active HTML injection in generated documents<![CDATA[ ## Vulnerability Details **File Location**: `scripts/format.py:1346-1367`, `scripts/format.py:1649-1685` **Vulnerability Type**: T09: Insecure Skill Coding Practices **Risk Level**: High ### Vulnerable Code ```python img_match = re.match(r'^!\[(.*?)\]\(\s*(\S+?)(?:\s+["\u201c\u2018\'](.*?)["\u201d\u2019\'])?\s*\)$', stripped) if img_match: flush_paragraph() alt = img_match.group(1) src = img_match.group(2) caption_text = (img_match.group(3) or "").strip() if ("封面" in alt) or alt.startswith("cover"): continue alt_escaped = html_mod.escape(alt) img_style = styles.get("img", "") or "max-width:100%; border-radius:4px;" img_html = ( f'<p style="text-align:center; margin:1.5em 0;">' f'<img src="{src}"{_image_dims(src, base_dir)} alt="{alt_escaped}" style="{img_style}" />' f'</p>' ) ``` ```python text = re.sub( r'(?<!!)\[(.+?)\]\((.+?)\)', rf'<a style="{a_style}" href="\2">\1</a>', text, ) text = re.sub(r'&lt;(https?://[^\s<>]+)&gt;|<(https?://[^\s<>]+)>', lambda m: f'<a style="{a_style}" href="{m.group(1) or m.group(2)}">' f'{m.group(1) or m.group(2)}</a>', text) links: list[str] = [] def _stash_link(m: re.Match) -> str: links.append(m.group(0)) return f"\x00L{len(links) - 1}\x00" text = re.sub(r'<a\s[^>]*>.*?</a>', _stash_link, text, flags=re.S) text = re.sub(r'(?<![\w"\'=/])(https?://[^\s<>"\',。、)】」]+)', lambda m: f'<a style="{a_style}" href="{m.group(1)}">{m.group(1)}</a>', text) text = re.sub(r"\x00L(\d+)\x00", lambda m: links[int(m.group(1))], text) text = re.sub(r"\x00C(\d+)\x00", lambda m: code_spans[int(m.group(1))], text) text = re.sub(r"\x00E(\d+)\x00", lambda m: escapes[int(m.group(1))], text) return text ``` The preformatter also explicitly preserves raw HTML tags: ```python _PREFORMAT_PROTECT_PATTERNS = ( re.compile(r"^[ \t]*```[^\n]*\n.*?^[ \t]*```[ \t]*$", re.M | re.S), re.compile(r"`[ ...[truncated 2960 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Escape ordinary Markdown text before applying controlled formatting: ```python text = html_mod.escape(text, quote=False) ``` Preserve only renderer-generated tags through internal placeholders. 2. Do not preserve arbitrary raw HTML by default. Either reject it or sanitize it using a strict allowlist of permitted tags and attributes. 3. Escape every value inserted into an HTML attribute: ```python safe_src = html_mod.escape(src, quote=True) safe_href = html_mod.escape(href, quote=True) ``` 4. Parse URLs with `urllib.parse.urlsplit()` and permit only explicitly approved schemes: - `https` - `http`, if required - Approved relative paths for local images Reject `javascript:`, `vbscript:`, unexpected `data:` URLs, protocol-relative URLs when inappropriate, and control characters. 5. Validate image paths separately from remote URLs. Local paths should be normalized and restricted to the article directory or another approved asset directory. 6. Sanitize configurable style strings or replace free-form styles with structured, validated style properties. 7. Add security regression tests covering: - Raw `script` elements. - SVG event handlers. - `img` elements with `onerror`. - Quotes in image and link destinations. - `javascript:` and encoded dangerous schemes. - Closing files and component content containing raw HTML. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/format.py:146
Finding
Theme and component path traversal allows access outside approved preset directories<![CDATA[ ## Vulnerability Details **File Location**: `scripts/format.py:146-152`, `scripts/format.py:752-776`, `scripts/format.py:1859-1880` **Vulnerability Type**: T05: Unauthorized Access and Privilege Escalation **Risk Level**: Medium ### Vulnerable Code Theme names are joined to trusted search directories without validating that they are simple filenames: ```python def _find_theme_file(name: str) -> Path | None: for d in THEME_SEARCH_DIRS: for ext in (".yaml", ".yml"): path = d / f"{name}{ext}" if path.exists(): return path return None ``` The skeleton value loaded from theme YAML is likewise treated as a relative directory without containment checks: ```python def _load_components(skeleton: str = "") -> dict: dirs = [BUILTIN_COMPONENTS_DIR] if skeleton: dirs.append(BUILTIN_COMPONENTS_DIR / skeleton) dirs.append(USER_COMPONENTS_DIR / skeleton) dirs.append(USER_COMPONENTS_DIR) out: dict[str, dict] = {} for d in dirs: if not d.is_dir(): continue for f in sorted(d.glob("*.yaml")) + sorted(d.glob("*.yml")): spec = _safe_yaml_dict(f) name = str(spec.get("name") or f.stem).strip() if name and spec.get("template"): out[name] = spec return out ``` The unvalidated theme value may originate from article-controlled YAML: ```python draft_dir = input_path.parent article_ctx = _load_article_context(draft_dir) if args.theme is None: preset = _coerce_single_preset("default_format_preset", article_ctx.get("default_format_preset")) theme_name = preset if preset else DEFAULT_THEME if preset: _info(f"主题来自本篇 article.yaml 的 default_format_preset: {theme_name}") else: theme_name = args.theme theme = _load_theme(theme_name) ``` ### Technical Analysis `_find_theme_file()` accepts an arbitrary string and appends `.yaml` or `.yml` before joining it to each search directory. T ...[truncated 2843 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Restrict theme and skeleton identifiers to simple names: ```python SAFE_NAME = re.compile(r"^[A-Za-z0-9_-]+$") def validate_name(value: str, field: str) -> str: if not SAFE_NAME.fullmatch(value): _err(f"Invalid {field}") return value ``` If Unicode preset names are required, explicitly permit approved Unicode letters while still rejecting separators, `.` traversal components, and control characters. 2. Resolve and verify every candidate path before accessing it: ```python base = d.resolve() candidate = (base / f"{name}.yaml").resolve() try: candidate.relative_to(base) except ValueError: _err("Theme path escapes the approved directory") ``` 3. Apply the same containment check to skeleton component directories. 4. Reject absolute paths and any value containing `/`, `\`, `..`, NUL characters, or platform-specific drive prefixes. 5. Consider opening files with protections against symlink races. At minimum, resolve symlinks and enforce containment after resolution. 6. Do not scan arbitrary directories based on theme-controlled metadata. Prefer a predefined mapping from approved skeleton identifiers to component directories. 7. Add tests for: - `../` traversal. - Absolute POSIX paths. - Windows drive and UNC paths. - Backslash traversal. - Symlinks escaping the approved directory. - Traversal in both `default_format_preset` and `skeleton`. ]]>
Vulnerability Patterns
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • 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 (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The declared description presents a full公众号排版 tool focused on Markdown-to-HTML conversion and article layout/theme styling. The supplied code instead is a narrowly scoped asset-generation script: it computes SVG paths for brush-stroke, dry-brush, wedge, and sine decorations and can emit preview HTML pages for those SVGs. While such assets could support a larger formatting system, this chunk does not implement the described core functionality. Its primary purpose is materially different from the declared skill behavior, so this is a clear description-behavior mismatch.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Vague Triggers

Medium
Confidence
94% confidence
Finding
The trigger list includes very broad everyday terms such as '格式化', '弄好看点', and '调整格式', which can cause the skill to activate in contexts unrelated to WeChat article formatting. Over-broad routing increases the chance that an agent performs unintended file reads/writes or transforms user content unexpectedly, especially in multi-skill environments where invocation determines what local artifacts are touched.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The documentation explicitly states that only the quoted title in Markdown image syntax is used as a caption and that alt-text/description should not be shown to readers. Later, the output behavior claims '图注自动从标记描述中提取', which directly contradicts that earlier intent and behavior description.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The display name is specified only in Chinese ("要点清单"), indicating a fixed language choice in user-facing metadata. There is no accompanying option, fallback, or documentation showing that users can choose their preferred language or locale.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The file uses Chinese for the display name and all natural-language guidance, which effectively forces a specific language for users consuming this skill definition. The policy allows locale constraints only when they are explicitly offered as a choice or clearly documented as justified for a region-specific use case, which is not present here.

Natural-Language Policy Violations

Medium
Confidence
91% confidence
Finding
This manifest contains user-facing natural-language fields such as displayName, when_to_use, when_not_to_use, anti_pattern, and example entirely in Chinese. Under the stated policy, forcing a specific language without user opt-in or a documented regional justification is a natural-language policy violation.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The user-facing metadata and instructional content are entirely in Chinese, with no indication that the skill supports other languages or that Chinese is an intentional, user-selected locale. This can violate language/locale policy when a skill implicitly forces one language for all users without offering choice or documenting the constraint.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The display name and all user-facing descriptive fields are written only in Chinese, including usage guidance and the example content. This imposes a language choice on users without any opt-in or indication that the skill is intentionally limited to a Chinese-speaking context.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The template renders the fixed string "— 完 —", which enforces a specific language/locale presentation in the output. The file contains no indication that this component is intentionally region-specific or that users can opt into another language.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
This manifest contains user-facing descriptive fields such as displayName, when_to_use, when_not_to_use, anti_pattern, and example entirely in Chinese. Per the policy, forcing a specific language without offering a choice or documenting a justified locale constraint is a natural-language policy violation.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The skill name, display name, and descriptive metadata are entirely in Chinese, which indicates a fixed language/locale for the skill experience. Under the policy, forcing a specific language without user opt-in or a documented region-specific justification is a natural-language policy violation.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The title and the entire document are written in Chinese and frame the guidance specifically around Chinese typography and WeChat publishing conventions, without any indication that users may choose another language or locale. Under the policy rule for language/locale constraints, this is a natural-language policy concern because the file imposes a specific language context without explicit user opt-in or an offered alternative.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This is a code file, so only SQP-2 and SQP-3 apply. The module docstring and usage/help text are entirely in Chinese, and the script's user-facing messages throughout the file are also Chinese-only, which effectively imposes a language/locale constraint on users without any opt-in or explicit justification that the skill is limited to a Chinese-language audience.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The description is written entirely in Chinese and targets formatting for WeChat public account articles, which implies a fixed language/locale context without stating that the skill is region-specific or optional. Under the policy, language or locale constraints should be justified or offered as a user choice.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The manifest grants "shell" permission to a skill whose stated purpose is Markdown-to-HTML formatting, and the manifest provides no justification for command execution. Unnecessary shell access materially expands the attack surface because any prompt injection or malicious content reaching the skill could be transformed into arbitrary command execution on the host environment.

Intent-Code Divergence

Low
Confidence
89% confidence
Finding
The workflow section states that when no theme is configured, the built-in theme name defaults to 'default'. The options table later says omission falls back to built-in default '块', which is a direct contradiction about actual fallback behavior.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
This file contains user-facing natural-language fields such as displayName, when_to_use, when_not_to_use, anti_pattern, and example entirely in Chinese. Under the policy, forcing a specific language without user opt-in or justification is a natural-language policy violation.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
This SVG contains user-facing text exclusively in Chinese (weekday labels and chart title) with no indication that the asset is intended only for a Chinese-language or region-specific context. Under the policy, forcing a specific language without opt-in or documented justification is a natural-language locale violation.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
This file contains user-visible text literals in Chinese ("行高 1.4" and "行高 1.95") with no indication that the asset is intentionally locale-specific or that alternative language variants exist. The policy allows justified region-specific constraints, but no such justification appears in this file.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
The file is written entirely in Chinese and does not offer an alternative language or indicate that the skill is intentionally limited to Chinese-speaking users. Under the policy criteria, forcing a specific language without user opt-in can be a natural-language policy violation.

Intent-Code Divergence

Low
Confidence
92% confidence
Finding
At L028-L029 the document explicitly states that `position` and `id` have not been tested. Later, L123-L124 and L290-L290 assert definite outcomes (`position` is removed, `id` is deleted), and L358-L358 again lists them as untested, creating an active contradiction within the documentation about what was actually verified.

Natural-Language Policy Violations

Low
Confidence
78% confidence
Finding
The file’s natural-language guidance is entirely in Chinese and includes normative statements about what 'reads right' in Chinese typography, such as prescribing what works '在中文里' and '中文标题下'. This creates a language/locale-specific constraint without offering a choice or clearly documenting that the tool is intentionally region-specific.

Static analysis

No suspicious patterns detected.