Back to skill

Security audit

Zhy Wechat Publish

Security checks for vulnerabilities and agentic risk

Overview

This skill matches its stated WeChat draft-publishing purpose, but it has real review-worthy risks around broad .env handling and unbounded image fetching/file reads from article HTML.

Install only if you are comfortable giving the skill WeChat Official Account credentials and letting it upload article content and images to WeChat. Keep its .env isolated in the skill directory, avoid running it from sensitive project roots, do not use --write-env unless you have checked the target .env file, and only process trusted HTML or HTML whose image paths and URLs you have reviewed.

Vulnerability Patterns
  • 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
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/wechat_draft.js:167
Finding
Unrestricted Remote Image Retrieval Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/wechat_draft.js`, lines 167–178 and 342–365 **Vulnerability Type**: Server-Side Request Forgery through attacker-controlled image URLs **Risk Level**: High ### Vulnerable Code ```js async function downloadBinary(url, redirects = 3) { const { statusCode, headers, body } = await requestRaw(url, { method: 'GET' }); if ([301, 302, 303, 307, 308].includes(statusCode) && headers.location && redirects > 0) { const nextUrl = new URL(headers.location, url).toString(); return downloadBinary(nextUrl, redirects - 1); } if (statusCode < 200 || statusCode >= 300) { throw new Error(`下载图片失败: HTTP ${statusCode}`); } return { data: body, contentType: headers['content-type'] || 'application/octet-stream' }; } ``` ```js async function rewriteImagesForWechat(token, html, htmlFilePath) { const imgRegex = /<img\b([^>]*?)\bsrc="([^"]+)"([^>]*)>/g; const matches = [...html.matchAll(imgRegex)]; if (!matches.length) return html; const srcMap = new Map(); for (const match of matches) { const src = match[2]; if (srcMap.has(src) || src.startsWith('data:')) continue; let fileName = 'image.png'; let fileData; let contentType = 'application/octet-stream'; if (/^https?:\/\//i.test(src)) { const downloaded = await downloadBinary(src); fileData = downloaded.data; const urlObj = new URL(src); fileName = path.basename(urlObj.pathname) || fileName; contentType = inferMimeType(fileName, downloaded.contentType); } else { ``` ### Technical Analysis The script treats every HTTP or HTTPS image source found in the supplied HTML as a trusted remote resource. It sends a request to the URL without validating its hostname, resolved IP address, port, or destination network. Consequently, crafted HTML can cause the machine running the Skill ...[truncated 2376 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allow remote retrieval only when explicitly enabled. 2. Require HTTPS and maintain an explicit allowlist of trusted image hosts. 3. Resolve the hostname before connecting and reject all loopback, private, link-local, multicast, reserved, and unspecified IPv4 and IPv6 addresses. 4. Repeat hostname and resolved-address validation after every redirect. 5. Protect against DNS rebinding by connecting only to the validated resolved address while preserving the expected TLS hostname. 6. Reject URLs containing credentials, unexpected ports, or unsupported schemes. 7. Enforce strict response limits: - Maximum download size - Connection, response, and total timeouts - Maximum redirect count 8. Validate the actual file signature and permit only supported image formats rather than trusting the URL extension or `Content-Type` header. 9. Consider downloading remote images in a sandboxed service with no access to internal networks. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/wechat_draft.js:342
Finding
HTML Image Paths Permit Reads Outside the Article Asset Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/wechat_draft.js`, lines 342–369 **Vulnerability Type**: Path traversal leading to arbitrary local file reads **Risk Level**: High ### Vulnerable Code ```js async function rewriteImagesForWechat(token, html, htmlFilePath) { const imgRegex = /<img\b([^>]*?)\bsrc="([^"]+)"([^>]*)>/g; const matches = [...html.matchAll(imgRegex)]; if (!matches.length) return html; const srcMap = new Map(); for (const match of matches) { const src = match[2]; if (srcMap.has(src) || src.startsWith('data:')) continue; let fileName = 'image.png'; let fileData; let contentType = 'application/octet-stream'; if (/^https?:\/\//i.test(src)) { const downloaded = await downloadBinary(src); fileData = downloaded.data; const urlObj = new URL(src); fileName = path.basename(urlObj.pathname) || fileName; contentType = inferMimeType(fileName, downloaded.contentType); } else { const localPath = path.resolve(path.dirname(htmlFilePath), src); if (!fs.existsSync(localPath)) { throw new Error(`正文图片不存在: ${localPath}`); } fileData = fs.readFileSync(localPath); fileName = path.basename(localPath); contentType = inferMimeType(fileName); } const wechatUrl = await uploadArticleImage(token, fileName, fileData, contentType); ``` ### Technical Analysis For every image source that does not begin with HTTP or HTTPS, the script resolves the value relative to the HTML file and reads the resulting path. There is no check that the canonical path remains within the article directory or another approved asset directory. `path.resolve()` normalizes traversal components but does not prevent them. A source such as `../../private/secret.png` can therefore resolve to any location accessible to the process. Absolute paths ...[truncated 2048 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define a dedicated, trusted asset root for local article images. 2. Resolve both the asset root and candidate path with `fs.realpathSync()` to account for symbolic links. 3. Verify containment using `path.relative()`: ```js const root = fs.realpathSync(path.dirname(htmlFilePath)); const candidate = fs.realpathSync(path.resolve(root, src)); const relative = path.relative(root, candidate); if (relative.startsWith('..') || path.isAbsolute(relative)) { throw new Error('Image path escapes the approved asset directory'); } ``` 4. Reject absolute paths before resolution. 5. Reject null bytes, filesystem URL forms, and unexpected path schemes. 6. Permit only regular files; reject directories, devices, sockets, and named pipes using `fs.statSync()`. 7. Enforce a conservative maximum file size before reading. 8. Validate image signatures and dimensions before upload. 9. Run the publisher under a minimally privileged operating-system account with no access to unrelated sensitive files. ]]>

T08 · Insecure Dependencies

Warning
Location
README.md:8
Finding
Installation Instructions Use Unpinned Executable Dependencies and Repository Revisions<![CDATA[ ## Vulnerability Details **File Location**: `README.md`, line 8 **Vulnerability Type**: Unpinned package and source-repository supply chain dependency **Risk Level**: Medium ### Vulnerable Code ```bash npx skills add https://github.com/zhylq/yuan-skills --skill zhy-wechat-publish ``` ### Technical Analysis The documented installation command invokes `npx skills` without specifying a package version. Depending on local npm behavior and cache state, this may retrieve and execute the currently published version of the `skills` package. The GitHub repository is also referenced without a release tag or immutable commit SHA. Its contents can therefore change after this Skill version has been reviewed. The effective installation process depends on two moving upstream components: 1. The current npm resolution of the `skills` executable 2. The current state of the referenced GitHub repository and branch This is a supply chain weakness rather than evidence that either current upstream source is malicious. Exploitation requires compromise or malicious modification of an upstream package, maintainer account, repository, or release process. ### Attack Path 1. An attacker compromises the npm package, its maintainer account, or the referenced GitHub repository. 2. The attacker publishes a malicious package version or modifies the repository revision selected by default. 3. A user follows the documented installation command. 4. `npx` resolves and runs the current package version, and the installer retrieves the current repository contents. 5. The altered installer or Skill content executes or is installed without being tied to the revision that was audited. ### Impact Assessment The impact depends on the behavior of the compromised installer or repository content. Because installation tooling and installed Skill scripts may execute with the user's privileges, a successful supply chain compromise could potentially: - Read files available to the user - Acces ...[truncated 312 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin the npm installer to an explicitly reviewed version, for example: ```bash npx --yes skills@<reviewed-version> add ... ``` 2. Pin the GitHub source to an immutable commit SHA rather than a mutable default branch. 3. Prefer signed, versioned release artifacts with published checksums. 4. Document the expected commit identifier and checksum so users can verify provenance before installation. 5. Use a lockfile where installation is part of a larger Node.js project. 6. Periodically review dependency ownership, release history, and security advisories. 7. Avoid executing newly resolved package versions automatically in privileged or sensitive environments. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (33)

Credential Access

High
Category
Privilege Escalation
Content
请先复制:

```bash
cp .env.example .env
```

### 最小必填
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Ae1

High
Category
analysis-evasion
Content
本 skill 适合与 `npx skills add ... --skill zhy-wechat-publish` 一起安装到 OpenCode、Claude Code 等支持 `SKILL.md` 的 agent 工具中。
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `upload_image.js` | 上传本地封面图到永久素材库,获取 `media_id` |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| `upload_image.js` | 上传本地封面图到永久素材库,获取 `media_id` |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
const candidates = [
            path.resolve(skillDir, '.env'),
            path.resolve(process.cwd(), '.env'),
            path.resolve(skillDir, '..', '..', '..', '.env'),
        ];
        for (const envPath of candidates) {
            if (!fs.existsSync(envPath)) continue;
Confidence
72% confidence
Finding
Searching upward multiple directories for a .env file can cause the script to ingest credentials from an unrelated parent project or user workspace without explicit consent. In a skill context that spawns child processes with the full inherited environment, this broad credential discovery increases the blast radius if the surrounding toolchain is compromised or misused.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
try {
        const skillDir = path.resolve(__dirname, '..');  // .claude/skills/wechat-draft/
        const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const candidates = [
            path.resolve(skillDir, '.env'),               // Skill 目录(最优先)
            path.resolve(process.cwd(), '.env'),          // 当前工作目录
            path.resolve(skillDir, '..', '..', '..', '.env'), // 项目根目录
        ];
        for (const envPath of candidates) {
            if (fs.existsSync(envPath)) {
Confidence
78% confidence
Finding
The script searches upward to a likely project-root .env file and loads every key it finds, expanding trust beyond the skill’s own directory. In an agent or multi-project environment, this can unintentionally consume unrelated secrets from a parent project and then use them in network operations, increasing the blast radius of misconfiguration or environment confusion.

Credential Access

High
Category
Privilege Escalation
Content
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
    const data = await requestJson(url, { method: 'GET' });
    if (data.errcode) {
        throw new Error(`获取 Access Token 失败: [${data.errcode}] ${data.errmsg}`);
    }
    return data.access_token;
}
Confidence
81% confidence
Finding
The app secret is embedded directly in the URL query string when requesting the token. Even though the destination is legitimate and HTTPS is used, query-string secrets are more likely to be captured in logs, proxies, telemetry, browser/history-like tooling, or error reporting systems than secrets sent in headers or bodies.

Credential Access

High
Category
Privilege Escalation
Content
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
}

        if (autoWriteEnv) {
            // 寻找 .env 文件并回写(优先写入 Skill 目录)
            const skillDir = path.resolve(__dirname, '..');
            const candidates = [
                path.resolve(skillDir, '.env'),
Confidence
84% confidence
Finding
The --write-env feature rewrites whichever .env file is found first across several locations, including the current directory and a parent project root. In shared or automated environments, this can modify unrelated configuration files, corrupt secrets/configuration, or create confusing cross-project state that later workflows trust.

Credential Access

High
Category
Privilege Escalation
Content
const skillDir = path.resolve(__dirname, '..');
            const candidates = [
                path.resolve(skillDir, '.env'),
                path.resolve(process.cwd(), '.env'),
                path.resolve(skillDir, '..', '..', '..', '.env'),
            ];
            for (const envPath of candidates) {
Confidence
83% confidence
Finding
Including process.cwd() as an implicit write target allows the script to alter the caller’s project configuration based solely on execution context. In agent-driven execution this is especially risky because the working directory may vary, causing unintended mutation of another repository’s .env file.

Credential Access

High
Category
Privilege Escalation
Content
const candidates = [
                path.resolve(skillDir, '.env'),
                path.resolve(process.cwd(), '.env'),
                path.resolve(skillDir, '..', '..', '..', '.env'),
            ];
            for (const envPath of candidates) {
                if (fs.existsSync(envPath)) {
Confidence
85% confidence
Finding
Writing back to a parent/root .env file is dangerous because it crosses project boundaries and can silently alter higher-level configuration. In a skill context, this is more dangerous than a normal standalone CLI because the skill may run inside larger automation where repository-root secrets and config are especially sensitive.

Credential Access

High
Category
Privilege Escalation
Content
const skillDir = path.resolve(__dirname, '..');
        const candidates = [
            path.resolve(skillDir, '.env'),
            path.resolve(process.cwd(), '.env'),
            path.resolve(skillDir, '..', '..', '..', '.env'),
        ];
        for (const envPath of candidates) {
Confidence
87% confidence
Finding
The script searches process.cwd() for a .env file, which broadens secret access beyond the skill's own configuration and may unintentionally ingest credentials from whatever directory the user runs the tool in. In agent or shared-workspace contexts, this can cause over-collection of unrelated secrets and accidental transmission or misuse under the wrong account.

Credential Access

High
Category
Privilege Escalation
Content
const candidates = [
            path.resolve(skillDir, '.env'),
            path.resolve(process.cwd(), '.env'),
            path.resolve(skillDir, '..', '..', '..', '.env'),
        ];
        for (const envPath of candidates) {
            if (fs.existsSync(envPath)) {
Confidence
93% confidence
Finding
The script also searches three directories above the skill for a .env file, which is unusually broad and can capture secrets from a parent project, monorepo root, or unrelated workspace. In an agent environment this materially increases the blast radius of credential exposure by letting the skill read secrets not intended for it.

Credential Access

High
Category
Privilege Escalation
Content
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
    const data = await requestJson(url, { method: 'GET' });
    if (data.errcode) {
        throw new Error(`获取 Access Token 失败: [${data.errcode}] ${data.errmsg}`);
    }
    return data.access_token;
}
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Rp1

Medium
Category
MCP Rug Pull
Confidence
93% confidence
Finding
The README instructs users to install and run a remote skill directly via `npx skills add` from a GitHub source without any pinned commit, tag, or checksum. That creates a supply-chain risk: if the upstream repository changes or is compromised, future installs may fetch different code than originally reviewed.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill explicitly depends on secrets stored in a local `.env` file (`WECHAT_APP_ID`, `WECHAT_APP_SECRET`) but declares no `permissions` or `allowed-tools` scope to signal or constrain environment access. In agent environments, missing scope declarations can lead to overbroad secret exposure or make it unclear to users that the skill needs credential access before execution.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/publish_with_cover.js:132