Back to skill

Security audit

wechat-style-publisher

Security checks for vulnerabilities and agentic risk

Overview

The skill’s WeChat publishing purpose is real, but it has under-scoped credential, network, and local-file upload behavior that users should review before installing.

Install only if you trust the operator, configuration files, and article HTML inputs. Keep real WeChat secrets out of shared workspaces, restrict token-cache permissions, use the default official WeChat API endpoint unless you fully trust a proxy, and do not run template imports or publishing on untrusted URLs or HTML without sandboxed network and filesystem access.

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/publish-node.mjs:130
Finding
Arbitrary Local File Read and Network Disclosure Through Article Image Paths<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish-node.mjs:130-142`; `scripts/publish-python.py:199-209` **Vulnerability Type**: Unrestricted local file access and upload **Risk Level**: High ### Complete Code Snippets Node.js implementation: ```javascript async function processContentImages(config, accessToken, html, baseDir) { const srcMatches = [...html.matchAll(/<img[^>]*src=["']([^"']+)["'][^>]*>/gi)]; let processed = html; for (const match of srcMatches) { const src = match[1]; if (/^https?:\/\//i.test(src) || src.startsWith("data:")) { continue; } const imagePath = path.isAbsolute(src) ? src : path.resolve(baseDir, src); const uploaded = await uploadImage(config, accessToken, imagePath, false); if (uploaded.url) { processed = processed.replaceAll(`src="${src}"`, `src="${uploaded.url}"`); processed = processed.replaceAll(`src='${src}'`, `src='${uploaded.url}'`); } } return processed; } ``` Python implementation: ```python async def process_content_images(self, access_token: str, content: str, content_dir: Path) -> str: processed = content matches = re.findall(r'<img[^>]*src=["\']([^"\']+)["\'][^>]*>', content, flags=re.IGNORECASE) for src in matches: if src.startswith(("http://", "https://", "data:")): continue image_path = Path(src) if not image_path.is_absolute(): image_path = (content_dir / src).resolve() uploaded = await self.upload_image(access_token, str(image_path), is_thumb=False) if uploaded.get("url"): processed = processed.replace(f'src="{src}"', f'src="{uploaded["url"]}"') processed = processed.replace(f"src='{src}'", f"src='{uploaded['url']}'") return processed ``` ### Technical Analysis Both publishers treat every non-HTTP and non-`data:` image source as a local filesystem path. Absolute paths are accepted directly, while relative paths are resolved withou ...[truncated 1819 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Reject absolute image paths from article HTML. 2. Resolve each relative path against a single explicitly approved content root. 3. Verify containment after canonicalization: ```javascript const root = await fs.realpath(baseDir); const candidate = await fs.realpath(path.resolve(root, src)); const relative = path.relative(root, candidate); if (relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error("Image path escapes the approved content directory"); } ``` 4. Apply the equivalent Python check with `Path.resolve()` and `Path.is_relative_to()`: ```python root = content_dir.resolve(strict=True) candidate = (root / src).resolve(strict=True) if not candidate.is_relative_to(root): raise ValueError("Image path escapes the approved content directory") ``` 5. Reject symbolic links that resolve outside the approved root. 6. Require an allowlisted extension and validate the file's actual magic bytes as JPEG, PNG, GIF, or another explicitly supported image format. 7. Impose per-file and aggregate upload size limits. 8. Consider requiring explicit image-file arguments rather than automatically reading every local path embedded in HTML. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish-node.mjs:75
Finding
Configurable API Origin Can Receive WeChat Application Secrets, Access Tokens, and Article Data<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish-node.mjs:75-119,150-176`; `scripts/publish-python.py:150-195,214-237`; `assets/config.example.json:2-5` **Vulnerability Type**: Unvalidated sensitive-data destination **Risk Level**: High ### Complete Code Snippets Node.js token request and upload destination: ```javascript const params = new URLSearchParams({ grant_type: "client_credential", appid: account.appId, secret: account.appSecret }); const url = `${config.wechat.apiBaseUrl || "https://api.weixin.qq.com"}/cgi-bin/token?${params.toString()}`; const response = await fetch(url); const result = await response.json(); ``` ```javascript async function uploadImage(config, accessToken, imagePath, isThumb = false) { const resolved = path.resolve(imagePath); const fileName = path.basename(resolved); const type = isThumb ? "thumb" : "image"; const url = `${config.wechat.apiBaseUrl || "https://api.weixin.qq.com"}/cgi-bin/material/add_material?access_token=${accessToken}&type=${type}`; const data = await fs.readFile(resolved); const form = new FormData(); form.append("media", new Blob([data]), fileName); const response = await fetch(url, { method: "POST", body: form }); const result = await response.json(); if (result.errcode && result.errcode !== 0) { throw new Error(`Upload image failed: ${result.errcode} - ${result.errmsg}`); } return result; } ``` Python destination selection and token request: ```python def __init__(self, config: dict, account_id: str, config_dir: Path): self.config = config self.account_id = account_id self.config_dir = config_dir self.account = self.config["wechat"]["accounts"][account_id] self.base_url = self.config.get("wechat", {}).get("apiBaseUrl", "https://api.weixin.qq.com") token_cache_dir = self.config.get("wechat", {}).get("tokenCacheDir", "./.tokens") self.token_cache_dir = (config_dir / token_cache_dir).resolve() ``` ```python params ...[truncated 2495 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Use `https://api.weixin.qq.com` as a fixed production endpoint. 2. If endpoint customization is necessary, implement an explicit hostname allowlist. 3. Require HTTPS and reject: - Plain HTTP - Embedded URL credentials - Unexpected ports - Loopback, private, link-local, multicast, and reserved IP addresses 4. In the Node.js implementation, disable automatic redirects or validate every redirect target before following it. 5. Ensure all token, material, and draft operations remain on the same validated origin. 6. Require a separate, clearly named development flag before allowing test endpoints. 7. Display a prominent warning when credentials will be sent anywhere other than the official WeChat API. 8. Protect configuration files against unauthorized modification and verify their ownership and permissions before loading them. 9. Avoid logging complete request URLs because they contain application secrets or access tokens. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/import-template-node.mjs:126
Finding
Unrestricted Template URL Fetching Enables Server-Side Request Forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/import-template-node.mjs:126-140`; `scripts/import-template-python.py:106-113` **Vulnerability Type**: Server-side request forgery **Risk Level**: High ### Complete Code Snippets Node.js implementation: ```javascript async function loadHtml(args) { const input = getArg(args, "input", "html-file", "file"); const url = getArg(args, "url", "article-url", "link"); if (input) { return fs.readFile(path.resolve(input), "utf8"); } if (url) { const response = await fetch(String(url)); if (!response.ok) { throw new Error(`Fetch failed: ${response.status} ${response.statusText}`); } return response.text(); } throw new Error("Provide --input <html-file> or --url <wechat-article-url>"); } ``` Python implementation: ```python def load_html(input_path: str, url: str) -> str: if input_path: return Path(input_path).read_text(encoding="utf-8") if url: response = httpx.get(url, timeout=30) response.raise_for_status() return response.text raise ValueError("必须提供 --input 或 --url") ``` ### Technical Analysis Both importers issue a network request to a user-controlled URL without enforcing that it is a public WeChat article. They do not validate the scheme, hostname, resolved address, port, or response size. The Node.js `fetch` implementation also follows redirects by default, and no redirect destination is revalidated. A public URL can therefore redirect the importer to a private or link-local address. Although the imported response must contain extractable HTML blocks for the full import operation to succeed, the outbound request itself occurs before parsing. This is sufficient to probe or interact with internal HTTP services. Responses containing suitable HTML can also be emitted to standard output or written into template and analysis files. ### Attack Path 1. An attacker controls the URL passed through `--url`, `--article-url ...[truncated 1168 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allow only `https:` URLs. 2. Restrict imports to approved WeChat article hostnames when that matches the intended functionality. 3. Resolve the hostname before connecting and reject addresses in: - Loopback ranges - RFC 1918 private ranges - Link-local ranges - Multicast ranges - Reserved and unspecified ranges - IPv6 local, link-local, and unique-local ranges 4. Re-resolve and revalidate the destination at connection time to reduce DNS rebinding risk. 5. Disable redirects by default. If redirects are required, validate every redirect target and impose a low redirect limit. 6. Set maximum response-body and header sizes instead of loading unlimited response text into memory. 7. Require an HTML-compatible content type. 8. Apply connection, read, and total timeouts in both implementations. 9. Prefer downloading public articles through a dedicated, isolated network service with restricted egress. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/publish-node.mjs:81
Finding
Plaintext Credential and Access-Token Storage Without Permission Hardening<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish-node.mjs:81-107`; `scripts/publish-python.py:155-183`; `scripts/set-config.mjs:67-81`; `assets/config.example.json:5-13,34-35` **Vulnerability Type**: Insecure storage of sensitive information **Risk Level**: Medium ### Complete Code Snippets Node.js token-cache creation: ```javascript await fs.mkdir(tokenCacheDir, { recursive: true }); const cacheFile = path.join(tokenCacheDir, `token_cache_${accountId.replace(/[^\w\u4e00-\u9fff-]+/g, "_")}.json`); try { const raw = await fs.readFile(cacheFile, "utf8"); const cached = JSON.parse(raw); if (Date.now() / 1000 < Number(cached.expires_at || 0) - 300) { return cached.access_token; } } catch {} // ... const expiresAt = Math.floor(Date.now() / 1000) + Number(result.expires_in || 7200); await fs.writeFile(cacheFile, JSON.stringify({ access_token: result.access_token, expires_at: expiresAt }, null, 2), "utf8"); ``` Python token-cache creation: ```python token_cache_dir = self.config.get("wechat", {}).get("tokenCacheDir", "./.tokens") self.token_cache_dir = (config_dir / token_cache_dir).resolve() async def get_access_token(self) -> str: self.token_cache_dir.mkdir(parents=True, exist_ok=True) cache_file = self.token_cache_dir / f"token_cache_{re.sub(r'[^\\w\\u4e00-\\u9fff-]+', '_', self.account_id)}.json" if cache_file.exists(): try: cache = json.loads(cache_file.read_text(encoding="utf-8")) if time.time() < cache["expires_at"] - 300: return cache["access_token"] except Exception: pass # ... expires_at = time.time() + result["expires_in"] cache_file.write_text(json.dumps({"access_token": result["access_token"], "expires_at": expires_at}), encoding="utf-8") return result["access_token"] ``` Configuration editor write: ```javascript const configPath = path.resolve(args.config); const raw = await fs.readFile(configPath, "utf8"); const c ...[truncated 2407 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create the token-cache directory with owner-only permissions: ```javascript await fs.mkdir(tokenCacheDir, { recursive: true, mode: 0o700 }); await fs.chmod(tokenCacheDir, 0o700); ``` ```python self.token_cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700) self.token_cache_dir.chmod(0o700) ``` 2. Create token files atomically with mode `0600`, avoiding a window where permissive permissions may apply. 3. Before reading an existing cache or configuration file, verify: - It is a regular file - It is owned by the expected user - It is not a symbolic link - Group and world permission bits are not set 4. Preserve or strengthen secure permissions when `set-config.mjs` rewrites configuration. 5. Load application secrets from environment variables, an operating-system keychain, or a managed secret store instead of plaintext JSON. 6. Store only non-sensitive account references in the configuration file. 7. Add `.tokens` and real credential configuration files to `.gitignore`. 8. Document that credential files must not be committed, included in artifacts, or placed in shared directories. 9. Delete expired token-cache files and provide a credential-rotation procedure after suspected exposure. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (23)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared purpose emphasizes multi-account WeChat publishing, but the described behavior also includes remote URL fetching and template extraction while static analysis indicates the core publishing capability may not actually exist. This mismatch is dangerous because users and agents may trust the skill with credentials or public-content operations under false assumptions, while hidden or undeclared network ingestion expands the attack surface.

Credential Access

High
Category
Privilege Escalation
Content
const result = await response.json();

  if (result.errcode) {
    throw new Error(`Get access token failed for ${accountId}: ${result.errcode} - ${result.errmsg}`);
  }

  const expiresAt = Math.floor(Date.now() / 1000) + Number(result.expires_in || 7200);
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
result = response.json()

        if "errcode" in result:
            raise RuntimeError(f"获取 Access Token 失败: {result['errcode']} - {result['errmsg']}")

        expires_at = time.time() + result["expires_in"]
        cache_file.write_text(json.dumps({"access_token": result["access_token"], "expires_at": expires_at}), encoding="utf-8")
Confidence
88% confidence
Finding
The script persists the WeChat access token in a local JSON cache file without applying explicit file-permission controls, encryption, or other protections. On shared systems or weakly protected workspaces, another local user or process could read the cached token and use it to act against the associated WeChat account until expiry, which is more sensitive in this skill because it supports publishing to multiple official accounts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill advertises file, write, and network-capable scripts but does not declare any tool scope or permission boundaries. That makes the skill harder to audit and increases the chance an agent invokes it with broader capabilities than the user expects, especially given it handles publishing workflows and remote content fetching.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
This skill involves account-specific credentials and publishing to public WeChat official accounts, but the description lacks an explicit user warning about those sensitive operations. Without that warning, users may provide secrets or trigger publication without fully understanding the reputational and account-level consequences.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The natural-language content is entirely in Chinese and labels the section as a WeChat intro, which indicates a fixed language/locale choice embedded in the template. There is no user opt-in, language selection mechanism, or documented justification that this file is intentionally limited to a Chinese-language regional context.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
The script accepts an arbitrary --url and fetches it without any allowlist, scheme restriction, or network-boundary controls. In environments where this skill runs with internal network access, this can be abused for SSRF-style access to internal services or to retrieve unexpected remote content, which is broader than the stated WeChat publishing use case.

Description-Behavior Mismatch

Medium
Confidence
89% confidence
Finding
When --name is provided, the script persists introHtml, outroHtml, header/footer image metadata, and a detailed analysis object containing candidate article blocks. This stores substantial fragments of imported third-party content and derived metadata beyond simple publishing configuration, creating unnecessary data retention and potential privacy/copyright exposure if sensitive or proprietary article content is imported.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code sends the configured app ID and app secret to a remote WeChat API and then writes the returned access token to a local cache file. There is no confirmation prompt, user-facing log, or explanatory comment/docstring warning that credentials will be used over the network and that tokens will be persisted on disk.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script reads local files and uploads them to the WeChat API, and later submits full article content as a draft. Although publishing is part of the script's apparent purpose, there is no user-facing warning, log, or inline explanation that local image assets and composed content will be transmitted externally.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This code reads the appId and appSecret from configuration and immediately sends them to the WeChat token endpoint, then proceeds to publish content over the network. Although the module docstring states the script publishes articles, there is no inline user disclosure, confirmation, or warning near the credential use and outbound transmission path.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code performs safety-relevant configuration changes, including deleting a key and overwriting the target config file, but provides no user-facing disclosure beyond echoing the path that was changed. There is no confirmation prompt, warning comment/docstring, or explicit notice that the script mutates the specified file in place.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The skill can import templates from article URLs, which means it may fetch and process untrusted remote content, but the description does not warn about that behavior. This omission increases the risk of unexpected network access, privacy issues, or ingestion of hostile HTML/CSS during template extraction.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
This manifest-style JSON includes default Chinese text values such as the account name, positioning, topics, and digest suffix, which implies a fixed language/locale in the skill's natural-language behavior. There is no accompanying indication that users may choose another language or that the locale restriction is intentional and documented.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The HTML template contains fixed Chinese-language user-facing text, which implies the skill will present content in a specific language regardless of user preference. Under the language/locale policy, hard-coding a language without offering choice or documenting a justified locale restriction is a natural-language policy concern.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"node": ">=18"
  },
  "dependencies": {
    "highlight.js": "^11.11.1",
    "juice": "^11.0.3"
  }
}
Confidence
95% confidence
Finding
The dependency uses a caret range (^11.11.1), which allows newer compatible releases to be installed over time. This weakens build reproducibility and can unexpectedly introduce vulnerable or malicious upstream changes, especially in a publishing tool that processes untrusted article content.

Unverifiable Dependency: highlight.js has 2 known advisory(ies) (GHSA-7wwv-vh3v-89cq (ReDOS vulnerabities: multiple grammars); CVE-2020-26237 (Prototype Pollution in highlight.js)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
84% confidence
Finding
highlight.js has known historical advisories, including ReDoS and prototype-pollution issues, and the manifest does not guarantee which exact version will be installed. In this skill's context, syntax highlighting may be applied to user-supplied article content, which makes parser-related flaws more relevant because crafted content could trigger denial of service or unsafe object manipulation.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
  "dependencies": {
    "highlight.js": "^11.11.1",
    "juice": "^11.0.3"
  }
}
Confidence
95% confidence
Finding
The dependency uses a caret range (^11.0.3) instead of an exact version, so installs are not fully deterministic. This creates supply-chain risk because future installs may pull different code than was originally reviewed, including versions with security regressions.

Unverifiable Dependency: httpx has 2 known advisory(ies) (CVE-2021-41945 (Improper Input Validation in httpx); CVE-2021-41945 (Encode OSS httpx <=1.0.0.beta0 is affected by improper input validation in `http)), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
90% confidence
Finding
The dependency specification allows any httpx release from 0.27 up to, but not including, 1.0, rather than pinning a known-safe version. This makes the installed package version environment-dependent and prevents verification that a vulnerable release will not be selected, which is a supply-chain hygiene weakness even if 0.27 itself is newer than the cited 2021 advisories.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Content
def get_arg(namespace, *names):
    for name in names:
        value = getattr(namespace, name, None)
        if value not in [None, ""]:
            return value
    return None
Confidence
50% confidence
Finding
Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The script will perform an outbound HTTP request to any user-supplied URL and ingest the response without validation, restriction, or explicit warning. In an agent/tooling context, this can enable SSRF-like access to internal services or unexpected data exfiltration paths if higher-level controls do not constrain what URLs can be fetched.

Missing User Warnings

Low
Confidence
87% confidence
Finding
The script writes imported HTML, CSS, analysis JSON, and registry data to user-specified filesystem paths with no validation or warning. In an agent setting, if path arguments are influenced by untrusted input, this could overwrite arbitrary files accessible to the process, causing data loss or persistence of malicious content.

Natural-Language Policy Violations

Low
Confidence
91% confidence
Finding
Multiple user-facing strings, including errors and CLI help text, are only in Chinese, such as the theme error, token failure message, and argument descriptions. This forces a specific language experience without any opt-in or documented region-specific justification in the file.

Static analysis

Detected: suspicious.exposed_secret_literal

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
scripts/publish-node.mjs:106