Back to skill

Security audit

wechat-pack

Security checks for vulnerabilities and agentic risk

Overview

This WeChat packaging skill has a coherent purpose, but unsafe asset handling could copy unintended local files or fetch unsafe remote URLs when processing untrusted documents.

Review before installing or using with documents you did not create. Use it only on trusted local inputs, avoid documents containing remote image URLs or unusual local image paths, inspect the generated assets folder before sharing or publishing, and do not open generated HTML from untrusted sources in a sensitive browser context.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pack_wechat.py:142
Finding

Unrestricted Server-Side Request Forgery Through Image and Cover URLs

Content
View full analysis

Vulnerability Details

File Location: scripts/pack_wechat.py:142-153, scripts/pack_wechat.py:173-175, and scripts/pack_wechat.py:324-327
Vulnerability Type: Server-Side Request Forgery (SSRF)
Risk Level: High

Vulnerable Code

python
def _download_url(url: str, dest_dir: str) -> str:
    _ensure_dir(dest_dir)
    req = urllib.request.Request(url, headers={"User-Agent": "wechat-pack/1.0"})
    with urllib.request.urlopen(req) as resp:
        data = resp.read()
        ext = _guess_ext_from_content_type(resp.headers.get("Content-Type", ""))
    base = _slugify(os.path.basename(urllib.parse.urlparse(url).path))
    if not os.path.splitext(base)[1] and ext:
        base = f"{base}{ext}"
    filename = _unique_path(os.path.join(dest_dir, base))
    with open(filename, "wb") as f:
        f.write(data)
    return filename
python
if src.startswith("http://") or src.startswith("https://"):
    local_path = _download_url(src, assets_dir)
    new_src = os.path.relpath(local_path, os.path.join(assets_dir, "..", "wechat"))
python
if args.cover.startswith("http://") or args.cover.startswith("https://"):
    cover_path = _download_url(args.cover, cover_dir)

Technical Analysis

URLs embedded in source HTML or Markdown and URLs passed through the --cover argument are retrieved directly by the host. The implementation does not apply a hostname allowlist, resolve and validate destination IP addresses, restrict access to internal address ranges, or validate redirect destinations.

Consequently, an attacker who controls a source document or cover URL can cause the process to issue HTTP requests to loopback interfaces, private networks, link-local services, or cloud instance metadata endpoints. Because the response is written into the generated package, the behavior may also expose response content to the package recipient.

Attack Path

  1. An attacker crea ...[truncated 1061 chars]
Remediation
View remediation

Remediation Suggestions

  • Permit downloads only from explicitly trusted HTTPS hosts.
  • Resolve hostnames before connecting and reject loopback, private, link-local, multicast, unspecified, and reserved IP ranges for both IPv4 and IPv6.
  • Prevent DNS rebinding by connecting only to the validated resolved address while preserving TLS hostname verification.
  • Disable automatic redirects or validate every redirect destination with the same URL and IP policy.
  • Reject URLs containing credentials or malformed host representations.
  • If arbitrary external images are required, perform retrieval through an isolated proxy with strict egress controls.
  • Log rejected destinations without recording credentials or sensitive URL parameters.

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/pack_wechat.py:181
Finding

Arbitrary Local File Copy Through Unrestricted Image Paths

Content
View full analysis

Vulnerability Details

File Location: scripts/pack_wechat.py:181-190
Vulnerability Type: Arbitrary Local File Read and Copy
Risk Level: High

Vulnerable Code

python
else:
    # relative or absolute file path
    candidate = src
    if not os.path.isabs(candidate):
        candidate = os.path.join(input_dir, candidate)
    if os.path.exists(candidate):
        base = _slugify(os.path.basename(candidate))
        dest = _unique_path(os.path.join(assets_dir, base))
        shutil.copy2(candidate, dest)
        new_src = os.path.relpath(dest, os.path.join(assets_dir, "..", "wechat"))

Technical Analysis

Local image sources are treated as filesystem paths. Absolute paths are accepted directly, while relative paths are joined to the input directory without canonicalization or containment enforcement. Traversal components such as ../ can therefore escape the input directory.

The code only checks whether the path exists. It does not require the path to be a regular file, verify that it is located beneath an approved asset root, or validate that its contents are an image. Any file readable by the process can consequently be copied into the output package.

Attack Path

  1. An attacker supplies HTML or Markdown containing an image source such as <img src="/etc/passwd"> or <img src="../../.env">.
  2. _rewrite_images() treats the source as a local path.
  3. An absolute path is used directly, or traversal escapes from input_dir.
  4. os.path.exists() confirms that the target exists.
  5. shutil.copy2() copies the target into the output assets/ directory.
  6. The generated package is shared or published, exposing the copied file.

Impact Assessment

The attacker can obtain files readable by the user account running the Skill. Potential targets include environment files, application configuration, API credentials, private keys, source code, and operating-system ...[truncated 190 chars]

Remediation
View remediation

Remediation Suggestions

  • Reject absolute paths in document image references.
  • Canonicalize candidate paths with os.path.realpath() or pathlib.Path.resolve().
  • Require the canonical source path to remain beneath an explicitly approved asset directory.
  • Use os.path.commonpath() for containment checks rather than string-prefix comparisons.
  • Reject traversal attempts, symbolic links that resolve outside the approved root, devices, sockets, directories, and other non-regular files.
  • Validate file signatures with a trusted image decoder rather than relying only on extensions or MIME guesses.
  • Apply a maximum file-size limit before copying.
  • Consider requiring users to explicitly approve external local assets instead of discovering them automatically.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/pack_wechat.py:299
Finding

Unsanitized Active Content in Generated WeChat HTML

Content
View full analysis

Vulnerability Details

File Location: scripts/pack_wechat.py:88-116 and scripts/pack_wechat.py:299-317
Vulnerability Type: HTML Injection and Cross-Site Scripting
Risk Level: High

Vulnerable Code

python
if line.startswith("### "):
    close_lists()
    html_lines.append(f"<h3>{line[4:]}</h3>")
    continue
if line.startswith("## "):
    close_lists()
    html_lines.append(f"<h2>{line[3:]}</h2>")
    continue
if line.startswith("# "):
    close_lists()
    html_lines.append(f"<h1>{line[2:]}</h1>")
    continue

if re.match(r"^[-*] ", line):
    if not in_ul:
        close_lists()
        html_lines.append("<ul>")
        in_ul = True
    html_lines.append(f"<li>{line[2:]}</li>")
    continue
if re.match(r"^\d+\. ", line):
    if not in_ol:
        close_lists()
        html_lines.append("<ol>")
        in_ol = True
    item = re.sub(r"^\d+\. ", "", line)
    html_lines.append(f"<li>{item}</li>")
    continue

close_lists()
html_lines.append(f"<p>{line}</p>")
python
else:
    with open(input_path, "r", encoding="utf-8") as f:
        html_body = f.read()

html_body = _rewrite_images(html_body, assets_dir, input_dir)
html_body = _ensure_title(html_body, args.title or "")
html_body = _apply_inline_styles(html_body)

wrapped = f"<section style=\"{INLINE_STYLES['section']}\">\n{html_body}\n</section>\n"

article_path = os.path.join(wechat_dir, "article.html")
with open(article_path, "w", encoding="utf-8") as f:
    f.write(wrapped)

Technical Analysis

HTML inputs are read and written into article.html without sanitization. Styling and image rewriting do not remove scripts, event-handler attributes, iframes, forms, dangerous URL schemes, or other active elements.

The fallback Markdown converter also inserts source text directly in ...[truncated 1542 chars]

Remediation
View remediation

Remediation Suggestions

  • Pass all converter output through a mature HTML sanitizer using a strict allowlist.
  • Allow only the tags and attributes required for WeChat article formatting.
  • Remove script elements, event attributes, iframes, objects, embeds, forms, metadata, and other active elements.
  • Restrict URL-bearing attributes to explicitly allowed schemes and validated destinations.
  • Restrict inline CSS to an allowlist of safe properties and values.
  • Escape headings, list items, paragraphs, and title text with html.escape() in the fallback Markdown converter.
  • Apply sanitization after all conversion, title insertion, and image rewriting so no later step can reintroduce unsafe markup.
  • Add regression tests for script tags, event handlers, malformed markup, SVG payloads, CSS-based payloads, and dangerous URL schemes.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/pack_wechat.py:142
Finding

Unbounded Remote Download Enables Resource Exhaustion

Content
View full analysis

Vulnerability Details

File Location: scripts/pack_wechat.py:142-153
Vulnerability Type: Uncontrolled Resource Consumption
Risk Level: Medium

Vulnerable Code

python
def _download_url(url: str, dest_dir: str) -> str:
    _ensure_dir(dest_dir)
    req = urllib.request.Request(url, headers={"User-Agent": "wechat-pack/1.0"})
    with urllib.request.urlopen(req) as resp:
        data = resp.read()
        ext = _guess_ext_from_content_type(resp.headers.get("Content-Type", ""))
    base = _slugify(os.path.basename(urllib.parse.urlparse(url).path))
    if not os.path.splitext(base)[1] and ext:
        base = f"{base}{ext}"
    filename = _unique_path(os.path.join(dest_dir, base))
    with open(filename, "wb") as f:
        f.write(data)
    return filename

Technical Analysis

urlopen() is invoked without an explicit timeout, and resp.read() loads the complete response body into memory without a maximum size. The implementation also does not enforce Content-Length, stream into a bounded output file, validate the response as an image, or limit the cumulative size and number of downloaded assets.

A hostile or compromised server can delay the response indefinitely, stream an extremely large body, or return non-image data. This can consume process memory, disk capacity, network bandwidth, and execution time.

Attack Path

  1. An attacker embeds an image URL controlled by the attacker or supplies it through --cover.
  2. The packaging process connects to the remote endpoint.
  3. The endpoint responds extremely slowly, never completes, or streams a very large body.
  4. resp.read() waits for completion and accumulates the body in memory.
  5. The process stalls, is terminated due to memory pressure, or writes excessive data to the output directory.
  6. Repeated URLs or repeated Skill invocations amplify resource consumption.

Impact Assessment

Successful exploitation ...[truncated 315 chars]

Remediation
View remediation

Remediation Suggestions

  • Configure strict connection and read timeouts.
  • Stream responses in fixed-size chunks rather than calling unbounded resp.read().
  • Stop downloading when a per-file byte limit is reached.
  • Enforce a cumulative download-size and asset-count limit for each packaging operation.
  • Reject a declared Content-Length above the configured limit, while still enforcing the limit during streaming.
  • Validate allowed MIME types and decode downloaded files with a trusted image library.
  • Limit decoded pixel dimensions to prevent image decompression bombs.
  • Write to a temporary file and remove partial content after any validation or network failure.
  • Run network retrieval with operating-system resource limits and restricted egress where practical.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (7)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill documentation describes capabilities to read local files, write output folders, invoke a shell-accessible dependency (pandoc), and download remote images, but it does not declare any explicit tool scope or permission boundaries. This creates an authorization ambiguity where an agent may invoke broader file, network, or shell actions than the user expects, increasing the risk of unintended data access, overwrites, or remote content retrieval.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The trigger description is broad and can activate on general requests about WeChat article formatting, Markdown conversion, or Word-to-HTML conversion without clearly limiting when the skill should be used. Over-broad routing can cause the agent to invoke a skill with file, network, and shell-adjacent behavior in contexts where the user did not intend document packaging or remote asset fetching, increasing the chance of unnecessary sensitive file handling.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
79% confidence
Finding

For a packaging/conversion skill, spawning an external executable is a capability with broader execution implications than pure in-process formatting. The manifest does not mention reliance on subprocess execution or external tools, so this capability is not clearly justified by the stated scope alone.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/pack_wechat.py (reported line 55)May include surrounding context.

python
def _run_pandoc(input_path: str) -> str:
    try:
        result = subprocess.run(
            ["pandoc", input_path, "-f", "docx", "-t", "html"],
            check=True,
            capture_output=True,

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

Although the skill is described as local packaging, the code automatically fetches remote image URLs from document content and allows a remote cover URL. That creates unexpected network egress and can leak usage metadata or trigger server-side request behavior from untrusted document content, which is more dangerous in a supposedly local-only packaging context.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script fetches remote content and writes it into local asset/cover directories without clear user-facing disclosure at the point of action. In a packaging workflow, this can surprise users with network access, import untrusted external content into local outputs, and create compliance/privacy issues when handling sensitive documents.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
84% confidence
Finding

This code creates an output directory tree, copies the input file and assets, and writes generated HTML and metadata files. Although these writes are central to the tool's packaging purpose, there is no explicit user-facing disclosure beyond the generic argument descriptions about the extent of filesystem modifications and copied content.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.