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

Content
View full analysis
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 = /]*?)\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
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/wechat_draft.js:342
Finding

HTML Image Paths Permit Reads Outside the Article Asset Directory

Content
View full analysis
]*?)\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
View remediation

T08 · Insecure Dependencies

Warning
Location
README.md:8
Finding

Installation Instructions Use Unpinned Executable Dependencies and Repository Revisions

Content
View full analysis
Remediation
View remediation
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
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · README.md (reported line 25)May include surrounding context.

请先复制:

bash
cp .env.example .env

最小必填

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 29)May include surrounding context.

md
本 skill 适合与 `npx skills add ... --skill zhy-wechat-publish` 一起安装到 OpenCode、Claude Code 等支持 `SKILL.md` 的 agent 工具中。

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 40)May include surrounding context.

md
| `upload_image.js` | 上传本地封面图到永久素材库,获取 `media_id` |

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

Scanner excerpt · SKILL.md (reported line 133)May include surrounding context.

md
| `upload_image.js` | 上传本地封面图到永久素材库,获取 `media_id` |

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/publish_with_cover.js (reported line 16)May include surrounding context.

js
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;

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/publish_with_cover.js (reported line 14)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/publish_with_cover.js (reported line 15)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/upload_image.js (reported line 14)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/upload_image.js (reported line 15)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/upload_image.js (reported line 162)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/wechat_draft.js (reported line 18)May include surrounding context.

js
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'), // 项目根目录
        ];

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/upload_image.js (reported line 16)May include surrounding context.

js
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)) {

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/upload_image.js (reported line 73)May include surrounding context.

js
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;
}

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 132)May include surrounding context.

md
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/upload_image.js (reported line 121)May include surrounding context.

js
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/upload_image.js (reported line 130)May include surrounding context.

js
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/wechat_draft.js (reported line 392)May include surrounding context.

js
const appSecret = process.env.WECHAT_APP_SECRET;

    if (!appId || !appSecret) {
        console.error('错误: 未找到微信 API 凭证。请检查 .env 文件。');
        process.exit(1);
    }

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/upload_image.js (reported line 159)May include surrounding context.

js
}

        if (autoWriteEnv) {
            // 寻找 .env 文件并回写(优先写入 Skill 目录)
            const skillDir = path.resolve(__dirname, '..');
            const candidates = [
                path.resolve(skillDir, '.env'),

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/upload_image.js (reported line 163)May include surrounding context.

js
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) {

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/upload_image.js (reported line 164)May include surrounding context.

js
const candidates = [
                path.resolve(skillDir, '.env'),
                path.resolve(process.cwd(), '.env'),
                path.resolve(skillDir, '..', '..', '..', '.env'),
            ];
            for (const envPath of candidates) {
                if (fs.existsSync(envPath)) {

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/wechat_draft.js (reported line 19)May include surrounding context.

js
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) {

Credential Access

High
Category
Privilege Escalation
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.

Content

Scanner excerpt · scripts/wechat_draft.js (reported line 20)May include surrounding context.

js
const candidates = [
            path.resolve(skillDir, '.env'),
            path.resolve(process.cwd(), '.env'),
            path.resolve(skillDir, '..', '..', '..', '.env'),
        ];
        for (const envPath of candidates) {
            if (fs.existsSync(envPath)) {

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/wechat_draft.js (reported line 129)May include surrounding context.

js
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;
}

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.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.