Back to skill

Security audit

zhihu-to-wechat

Security checks for vulnerabilities and agentic risk

Overview

This skill is mostly aligned with its stated WeChat draft workflow, but it asks for powerful account credentials and handles them with weak scoping and storage safeguards.

Review this skill before installing. Use it only with a dedicated WeChat service account, avoid entering secrets in chat or command-line arguments, rotate any secrets already shared, inspect generated HTML before creating drafts, and do not let untrusted image URLs flow into the publisher.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/wechat_publisher.py:174
Finding
Arbitrary Image URL Fetching Enables SSRF and Local File Disclosure<![CDATA[ ## Vulnerability Details **File Location**: `scripts/wechat_publisher.py`, lines 174-216 and 226-256 **Vulnerability Type**: Server-Side Request Forgery (SSRF) and unrestricted local resource access **Risk Level**: High ### Vulnerable Code ```python def upload_image_from_url(self, image_url: str) -> str: """从 URL 下载图片并上传到微信素材库,返回微信 CDN URL""" token = self.get_access_token() # 下载图片 print(f" ⬇️ 下载图片: {image_url[:60]}...") try: with urllib.request.urlopen(image_url, timeout=15) as resp: image_data = resp.read() content_type = resp.headers.get("Content-Type", "image/jpeg") except Exception as e: raise RuntimeError(f"图片下载失败: {e}") from e # 确定文件扩展名 ext = "jpg" if "png" in content_type: ext = "png" elif "gif" in content_type: ext = "gif" elif "webp" in content_type: ext = "webp" # 上传到微信(使用 uploadimg 接口,返回永久 URL) upload_url = f"https://api.weixin.qq.com/cgi-bin/media/uploadimg?access_token={token}" boundary = "----FormBoundaryX7MA4YWxkTrZu0gW" filename = f"image.{ext}" body = ( f"--{boundary}\r\n" f'Content-Disposition: form-data; name="media"; filename="{filename}"\r\n' f"Content-Type: {content_type}\r\n\r\n" ).encode("utf-8") + image_data + f"\r\n--{boundary}--\r\n".encode("utf-8") req = urllib.request.Request( upload_url, data=body, headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, method="POST", ) with urllib.request.urlopen(req, timeout=30) as resp: result = json.loads(resp.read().decode("utf-8")) ``` The same issue is present in the cover-image path: ```python def upload_cover_image(self, image_url: str) -> str: """上传封面图并返回 thumb_media_id(用于草稿接口)""" token = self.get_access_token() # 下载图片 with urllib.request.urlopen(image_url, timeout=15) as resp: image_data = resp.read() content_type ...[truncated 2540 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `https` URLs. 2. Restrict downloads to an explicit allowlist of expected image CDN domains. 3. Resolve the hostname before connecting and reject loopback, private, link-local, multicast, unspecified, and reserved addresses for both IPv4 and IPv6. 4. Repeat destination validation after every redirect, or disable automatic redirects. 5. Prevent DNS rebinding by connecting to a validated resolved address while preserving certificate and hostname verification. 6. Reject URLs containing embedded credentials. 7. Stream the response with a strict byte limit instead of calling unbounded `resp.read()`. 8. Verify both the declared MIME type and image file signature using a trusted image parser. 9. Decode and re-encode images before upload to eliminate polyglot or malformed content. 10. Apply connection, read, and total-operation timeouts. 11. If arbitrary external images are required, perform retrieval in a sandbox with no private-network access and minimal filesystem permissions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/wechat_publisher.py:37
Finding
WeChat Access Token Stored in a Plaintext Cache Without Explicit Permission Controls<![CDATA[ ## Vulnerability Details **File Location**: `scripts/wechat_publisher.py`, lines 37 and 131-168 **Vulnerability Type**: Insecure storage of bearer credentials **Risk Level**: Medium ### Vulnerable Code ```python # Access Token 本地缓存文件 TOKEN_CACHE_FILE = Path.home() / ".wechat_token_cache.json" ``` ```python # 2. 检查文件缓存 if TOKEN_CACHE_FILE.exists(): try: cache = json.loads(TOKEN_CACHE_FILE.read_text()) if (cache.get("app_id") == self.app_id and cache.get("expires_at", 0) > time.time() + 300): self._access_token = cache["access_token"] self._token_expires_at = cache["expires_at"] print("📋 使用缓存 Access Token") return self._access_token except Exception: pass ``` ```python # 保存文件缓存 TOKEN_CACHE_FILE.write_text(json.dumps({ "app_id": self.app_id, "access_token": self._access_token, "expires_at": self._token_expires_at, })) ``` ### Technical Analysis The WeChat access token is a bearer credential that authorizes API operations. It is stored as plaintext in the user's home directory using `Path.write_text()`, with permissions inherited from the process umask. The code does not explicitly enforce owner-only permissions, verify the cache file's owner, reject symbolic links, or use atomic secure-file creation. In environments with a permissive umask, shared home-directory access, backups, or overprivileged monitoring agents, the token may be exposed to other local principals. The cache also includes the associated AppID and expiration time, making a stolen token immediately usable without additional discovery. ### Attack Path 1. The publisher obtains a valid WeChat access token. 2. The token is written to `~/.wechat_token_cache.json`. 3. File permissions are determined by the ambient umask rather than an explicit security policy. 4. Another local process, user, backup collector, or compromised component reads the cache. 5. The reader extracts the b ...[truncated 620 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer an operating-system credential store or secret-management service. 2. If a file cache is necessary, create it atomically with owner-only mode `0600`. 3. Open the cache using flags that prevent symbolic-link following where supported. 4. Verify that an existing file is owned by the current user, is a regular file, and has no group or world permissions before reading it. 5. Store the cache in a private directory with mode `0700`. 6. Use atomic replacement to avoid partial writes and race conditions. 7. Delete expired tokens promptly. 8. Avoid copying the cache into logs, backups, support bundles, or shared workspaces. 9. Document the credential sensitivity and local security assumptions. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/wechat_publisher.py:10
Finding
WeChat AppSecret Can Be Exposed Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/wechat_publisher.py`, lines 10-16 and 395-404 **Vulnerability Type**: Sensitive credential exposure through process arguments and shell history **Risk Level**: Medium ### Vulnerable Code ```python 用法: python wechat_publisher.py \ --app-id YOUR_APP_ID \ --app-secret YOUR_APP_SECRET \ --html article.html \ --title "文章标题" \ --cover cover.jpg \ --author "作者名" ``` ```python def main(): parser = argparse.ArgumentParser(description="微信公众号草稿发布工具") parser.add_argument("--app-id", default=os.getenv("WECHAT_APP_ID"), help="服务号 AppID") parser.add_argument("--app-secret", default=os.getenv("WECHAT_APP_SECRET"), help="服务号 AppSecret") parser.add_argument("--html", required=True, help="文章 HTML 文件路径") parser.add_argument("--title", required=True, help="文章标题") parser.add_argument("--cover", default="", help="封面图 URL 或本地路径") parser.add_argument("--author", default="IT科技号", help="作者名称") args = parser.parse_args() if not args.app_id or not args.app_secret: print("❌ 请设置 WECHAT_APP_ID 和 WECHAT_APP_SECRET 环境变量,或通过 --app-id/--app-secret 参数传入") return ``` ### Technical Analysis The script explicitly supports and documents passing the long-lived WeChat AppSecret on the command line. Command-line arguments may be recorded in: - Shell history - Process listings - Process accounting systems - CI/CD execution logs - Monitoring and endpoint telemetry - Debugging output from wrappers or orchestrators Unlike the short-lived access token, compromise of the AppSecret may allow an attacker to request new access tokens repeatedly until the secret is rotated. Although environment-variable input is also supported and described as recommended, the insecure CLI path remains available and is presented as a normal usage pattern. ### Attack Path 1. A user follows the documented command and supplies `--app-secret`. 2. The shell reco ...[truncated 830 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the `--app-secret` command-line option. 2. Retrieve the secret from a protected secret manager or operating-system credential store. 3. If interactive use is required, accept it through a non-echoing prompt such as `getpass.getpass()`. 4. If environment variables are retained, document that they may still be exposed by unsafe process-inspection or logging configurations. 5. Use short-lived workload credentials where platform support exists. 6. Ensure CI/CD systems inject the value as a masked secret and never interpolate it into command strings. 7. Rotate the AppSecret if it has previously been supplied through command-line arguments or recorded in logs. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/format_article.py:216
Finding
Unsanitized Markdown and Metadata Permit HTML and Unsafe Link Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/format_article.py`, lines 216-291 and 301-344 **Vulnerability Type**: HTML injection and unsafe URL generation **Risk Level**: Medium ### Vulnerable Code ```python # 图片占位符 [配图N占位:描述] img_match = re.match(r"^\[配图(\d+)占位:(.+?)\]$", line) if img_match: n = img_match.group(1) desc = img_match.group(2) html_lines.append( f'<img src="{{{{IMAGE_{n}}}}}" alt="{desc}" style="{STYLE_IMG}" />' f'<p style="{STYLE_IMG_CAPTION}">▲ {desc}</p>' ) continue ``` ```python # 标题 if line.startswith("# "): text = line[2:].strip() html_lines.append(f'<h1 style="{STYLE_H1}">{text}</h1>') elif line.startswith("## "): text = line[3:].strip() html_lines.append(f'<h2 style="{STYLE_H2}">{text}</h2>') elif line.startswith("### "): text = line[4:].strip() html_lines.append(f'<h3 style="{STYLE_H3}">{text}</h3>') ``` ```python # 普通段落 else: text = apply_inline_styles(line) if text.strip(): html_lines.append(f'<p style="{STYLE_P}">{text}</p>') ``` ```python def apply_inline_styles(text: str) -> str: """处理行内 Markdown 格式""" # 粗体 text = re.sub(r"\*\*(.+?)\*\*", f'<strong style="{STYLE_STRONG}">\\1</strong>', text) text = re.sub(r"__(.+?)__", f'<strong style="{STYLE_STRONG}">\\1</strong>', text) # 行内代码 text = re.sub(r"`(.+?)`", f'<code style="{STYLE_CODE_INLINE}">\\1</code>', text) # 斜体 text = re.sub(r"\*(.+?)\*", r'<em>\1</em>', text) # 链接 text = re.sub(r"\[(.+?)\]\((.+?)\)", f'<a href="\\2" style="color:{COLOR_PRIMARY}">\\1</a>', text) return text ``` ```python # 标签区 tags_html = "".join(f'<span style="{STYLE_TAG}">#{tag}</span>' for tag in tags) # 完整 HTML html = f"""<section style="{STYLE_BODY}"> {body_html} <p>{tags_html}</p> {build_footer(author)} </section>""" ``` The author is also inserted without escaping: ```python def build_footer(author: str = "IT科技号") -> str: """生成文章底部(版权+关注引导)""" return f""" ...[truncated 2400 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-escape every text value before placing it into element content. 2. Escape attribute values separately, including quotes. 3. Parse Markdown with a maintained library configured to disable raw HTML. 4. Sanitize the final output with an allowlist-based HTML sanitizer. 5. Permit only required tags and attributes compatible with WeChat. 6. Validate links using a URL parser and allow only approved schemes, normally `https`. 7. Reject protocol-relative URLs, control characters, encoded scheme bypasses, and malformed destinations. 8. Sanitize author names, tags, captions, and image descriptions as untrusted input. 9. Add tests for element injection, attribute breaking, dangerous URL schemes, malformed Markdown, and encoded payloads. 10. Retain WeChat's platform sanitization as defense in depth rather than the primary security boundary. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • 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
Findings (45)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
声明描述的是一个多阶段自动化内容生产与发布系统,核心能力包括热点发现、文章写作、配图和微信发布。但代码片段只包含 fetch_images.py,其行为局限于图片检索与返回图片元数据,不包含任何知乎热榜访问、文本生成、微信公众号接口调用或发布逻辑。虽然“自动配图”与声明中的一个子步骤一致,但无法支撑该 skill 所宣称的整体用途,因此描述与实际行为存在明显不匹配。

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
声明描述的是一个多步骤内容生产与发布系统,核心能力包括:知乎热榜选题、IT 风格文章生成、配图、以及微信服务号发布。但代码内容只包含对知乎热榜 API 的请求、结果解析、科技关键词标记和终端输出,没有任何文本生成、图片生成/获取、微信公众号接口调用、发布动作、账号凭据处理或工作流编排逻辑。因此,代码实际行为只覆盖了声明中的“获取知乎热榜并筛选科技话题”这一小部分,主功能与声明存在明显不一致。代码中的网络访问知乎 API 属于已声明工作流的支持步骤,不是额外越权能力;问题在于声明严重高估了已实现能力。

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
声明描述的是一个端到端自动化内容生产与发布流程,涵盖热点采集、写作、配图和发布多个阶段。但实际代码只是一个格式化脚本:读取本地 Markdown,按预设 IT 科技风样式转成微信公众号 HTML,并替换图片占位符后输出 HTML 文件。代码没有任何网络访问、知乎抓取、内容生成、图像生成/下载、微信公众号 API 调用或发布逻辑。因此,实际行为只覆盖了“公众号文章排版/格式化”这一小部分,无法支撑所宣称的主要用途,属于明显描述与行为不一致。

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
声明描述的是一个端到端内容生产与发布流水线,覆盖热点发现、内容创作、配图和发布;但代码实际是一个较窄的微信草稿发布工具。它依赖外部提供的 HTML 文章和图片 URL,只负责与微信公众平台 API 交互完成素材上传和草稿创建。虽然“发布公众号”这一小部分有一定相关性,但核心能力大幅缺失,且最终动作也只是进入草稿箱而非直接发布,因此声明与实际行为存在明显且实质性的不匹配。

Vague Triggers

High
Confidence
97% confidence
Finding
The skill mandates automatic activation for broad, common phrases related to writing and publishing. Overbroad trigger rules can cause the agent to invoke networked, credential-using, or publishing workflows in contexts where the user did not intend it, increasing the chance of data exposure, unwanted external requests, or accidental content publication.

Missing User Warnings

High
Confidence
97% confidence
Finding
The skill instructs collection of sensitive credentials such as WECHAT_APP_SECRET and third-party API keys and storing them in conversation context, without explicit privacy, retention, or redaction safeguards. Conversation context is often accessible to logs, future turns, or other components, so treating secrets as ordinary chat content materially increases the risk of credential leakage and account compromise.

Credential Access

High
Category
Privilege Escalation
Content
运行脚本:`scripts/wechat_publisher.py`

流程:
1. 获取 Access Token(自动刷新)
2. 上传封面图到微信素材库 → 获取 `thumb_media_id`
3. 上传正文配图 → 替换文章中的图片 URL 为微信 CDN 地址
4. 调用草稿接口创建草稿
Confidence
78% confidence
Finding
The publication step explicitly retrieves and refreshes a WeChat access token and then uses it to upload assets and create drafts. In the context of this skill, token handling is dangerous because the same document also normalizes storing related secrets in chat context, and publication credentials provide direct control over a public-facing account.

Credential Access

High
Category
Privilege Escalation
Content
## 目录
1. [权限要求](#权限要求)
2. [Access Token](#access-token)
3. [草稿箱接口](#草稿箱接口)
4. [素材上传](#素材上传)
5. [常见错误码](#常见错误码)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## 目录
1. [权限要求](#权限要求)
2. [Access Token](#access-token)
3. [草稿箱接口](#草稿箱接口)
4. [素材上传](#素材上传)
5. [常见错误码](#常见错误码)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## 目录
1. [权限要求](#权限要求)
2. [Access Token](#access-token)
3. [草稿箱接口](#草稿箱接口)
4. [素材上传](#素材上传)
5. [常见错误码](#常见错误码)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## 目录
1. [权限要求](#权限要求)
2. [Access Token](#access-token)
3. [草稿箱接口](#草稿箱接口)
4. [素材上传](#素材上传)
5. [常见错误码](#常见错误码)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## 目录
1. [权限要求](#权限要求)
2. [Access Token](#access-token)
3. [草稿箱接口](#草稿箱接口)
4. [素材上传](#素材上传)
5. [常见错误码](#常见错误码)
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
wechat_publisher.py — 微信服务号草稿发布工具

功能:
1. 获取/刷新 Access Token
2. 上传图片到微信素材库
3. 替换文章中的图片链接为微信CDN地址
4. 创建草稿
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill describes operations requiring network access, local file read/write, and secret handling, but it declares no explicit tool scope or permissions boundary. That makes the operational surface ambiguous and can allow broader-than-expected execution or review bypass in hosts that rely on manifest-declared scopes for enforcement.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The entire template is written as a Chinese-language article format for an IT public account and does not indicate that language selection is optional. This can violate language/locale policy when a skill should not force a specific language without user opt-in or documented regional justification.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The module docstring, usage instructions, and argument descriptions are presented in Chinese only, which effectively forces a single language for users interacting with the skill. The policy allows locale constraints only when justified or when users are offered a language choice, neither of which is present here.

External Transmission

Medium
Category
Data Exfiltration
Content
"orientation": "landscape",  # 横版图,适合公众号
        "content_filter": "high",    # 过滤低质量图片
    })
    url = f"https://api.unsplash.com/search/photos?{params}"
    req = urllib.request.Request(url, headers={
        "Authorization": f"Client-ID {access_key}",
        "Accept-Version": "v1",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
"orientation": "landscape",  # 横版图,适合公众号
        "content_filter": "high",    # 过滤低质量图片
    })
    url = f"https://api.unsplash.com/search/photos?{params}"
    req = urllib.request.Request(url, headers={
        "Authorization": f"Client-ID {access_key}",
        "Accept-Version": "v1",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.