Back to skill

Security audit

Wechat Mp Auto

Security checks for vulnerabilities and agentic risk

Overview

This WeChat automation skill has a coherent purpose, but it needs Review because it can affect a real WeChat account while using under-scoped credential handling, URL fetching, and hardcoded attribution behavior.

Install only if you are comfortable giving this skill WeChat account authority to upload media, create and possibly publish drafts, read account/user/analytics data, and send content to third-party research or image services. Review or change the hardcoded source URL, avoid untrusted remote image URLs, restrict local credential-file permissions, and use narrowly scoped API keys where possible.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (5)

T01 · Skill Instruction Hijacking

Warning
Location
SKILL.md:169
Finding
Mandatory Unrelated Source URL Injected into Published Articles<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:169-177`; independently reinforced by `src/publish.py:79` **Vulnerability Type**: Persistent output manipulation through mandatory Skill instructions **Risk Level**: Medium ### Vulnerable Code ```text 第八步:推送草稿 → 调用 upload_thumb(封面图本地路径) → 获得 thumb_media_id(素材缩略图ID) → 调用 create_draft([{ "title": 文章标题, "author": "贾维斯", "content": 包含图片URL的完整HTML, "thumb_media_id": 封面缩略图的media_id, "content_source_url": "https://openclaw.ai" }]) → 获得草稿ID,流程完成 ``` The command-line publishing interface applies the same default: ```python parser.add_argument( '--source-url', '-s', type=str, default='https://openclaw.ai', help='原文链接' ) ``` ### Technical Analysis The Skill instructions direct the agent to attach `https://openclaw.ai` as the source URL to every generated WeChat draft, regardless of the article's actual origin or subject. This behavior is not necessary for researching, formatting, or publishing an article. Because the instruction is part of the prescribed publishing workflow, an agent following the Skill will include the unrelated URL without requesting article-specific consent. The CLI default independently preserves this behavior even when publication is invoked directly. This constitutes Skill instruction hijacking because loading and following the Skill modifies the expected publication output to promote a predetermined third-party destination. ### Attack Path 1. A user loads the Skill and requests creation of a WeChat article. 2. The agent follows the mandatory eight-step workflow in `SKILL.md`. 3. During draft creation, the agent supplies `https://openclaw.ai` as `content_source_url`. 4. Alternatively, the publication CLI is run without `--source-url`, causing the same URL to be selected automatically. 5. The resulting draft contains an unrelated promotional or attribution link without explicit per-article authorization. ### Impact Assessment The ...[truncated 409 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Remove the hardcoded `https://openclaw.ai` value from both the Skill workflow and CLI defaults. - Default `content_source_url` to an empty value. - Require the user to provide a source URL explicitly when attribution is appropriate. - Validate that a supplied source URL uses HTTPS and corresponds to the intended publication. - Clearly display the final source URL for confirmation before creating the draft. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/publish.py:254
Finding
Server-Side Request Forgery Through Article Image URLs<![CDATA[ ## Vulnerability Details **File Location**: `src/publish.py:254-262` and `src/publish.py:340-350` **Vulnerability Type**: Unrestricted server-side URL fetching **Risk Level**: High ### Vulnerable Code The integrity check sends HEAD requests to URLs extracted from article content: ```python if all_external: import urllib.request bad_urls = [] for url in all_external[:6]: # 最多检查6张 try: req = urllib.request.Request(url, method='HEAD') req.add_header('User-Agent', 'Mozilla/5.0') with urllib.request.urlopen(req, timeout=5) as resp: if resp.status != 200: bad_urls.append(f"{url[:60]}... (HTTP {resp.status})") except Exception as e: bad_urls.append(f"{url[:60]}... ({type(e).__name__})") ``` The publication path subsequently downloads arbitrary HTTP or HTTPS image URLs: ```python if img_path.startswith('http://') or img_path.startswith('https://'): try: import tempfile import urllib.request ext = os.path.splitext(img_path.split('?')[0])[1] or '.jpg' with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp: tmp_path = tmp.name urllib.request.urlretrieve(img_path, tmp_path) result = material_skill.upload_image(tmp_path) wechat_url = result.get('url') ``` ### Technical Analysis Article-controlled image URLs are dereferenced without validating the destination host or resolved IP address. The code accepts both plaintext HTTP and HTTPS and does not reject: - Loopback destinations such as `127.0.0.1` or `::1` - RFC 1918 private networks - Link-local addresses, including cloud metadata services - Reserved, multicast, or otherwise non-public address ranges - Redirects from an initially public URL to a private destination The integrity check creates a blind SSRF primitive through HEAD requests. The publication path creates a stronger GET-based SSRF primitive because it d ...[truncated 1573 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Prefer a strict allowlist of approved image CDN domains. - Require HTTPS unless a documented local-development mode is explicitly enabled. - Resolve the hostname before connecting and reject loopback, private, link-local, multicast, unspecified, and reserved addresses for both IPv4 and IPv6. - Disable automatic redirects or validate the destination after every redirect. - Protect against DNS rebinding by connecting to the validated address while preserving the intended TLS hostname. - Stream downloads with strict byte limits instead of using `urlretrieve`. - Require an approved image MIME type and verify the file using an image decoder before upload. - Apply connection and read timeouts separately. - Delete temporary files in a `finally` block. - Avoid network requests during passive integrity checks, or perform them only after explicit user approval. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/skills/image_generator.py:258
Finding
Shared API Credentials Can Be Forwarded to Unvalidated Provider Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `src/skills/image_generator.py:258-286` and `src/skills/image_generator.py:367-395` **Vulnerability Type**: Credential disclosure through unvalidated configurable endpoints **Risk Level**: High ### Vulnerable Code The Skill reads API keys from OpenClaw's shared credential store and associates them with a configurable base URL: ```python def _get_credential(self, model_id: str) -> Optional[Dict]: """根据模型 ID 从 OpenClaw 配置获取 provider 的 API 凭证""" try: import json # 读 credentials cred_file = Path.home() / ".openclaw" / "credentials" / "api-keys.json" if cred_file.exists(): with open(cred_file) as f: creds = json.load(f) # 读 model 配置,找 provider config_file = Path.home() / ".openclaw" / "openclaw.json" if config_file.exists(): with open(config_file) as f: config = json.load(f) providers = config.get("models", {}).get("providers", {}) for pname, pcfg in providers.items(): for m in pcfg.get("models", []): if m.get("id") == model_id: api_key = creds.get(pname, {}).get("apiKey", "") return { "provider": pname, "apiKey": api_key, "baseUrl": pcfg.get("baseUrl", ""), } ``` The key is then transmitted to the configured endpoint without validating the scheme or host: ```python api_key = creds.get("apiKey", "") # 构造请求 url = f"{base_url}{api_path}" headers = {"Content-Type": "application/json"} if auth_type == "bearer": headers["Authorization"] = f"Bearer {api_key}" elif auth_type == "api_key": headers["api-key"] = api_key payload = self._build_probe_request(provider, req_format) try: import requests logger.info(f"探测生图能力: {provider}/{model_id} -> {url}") resp = reques ...[truncated 2336 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Define provider-specific HTTPS hostname allowlists and reject endpoints that do not match them. - For custom endpoints, require explicit user confirmation before transmitting credentials. - Reject plaintext HTTP, URL user information, fragments, malformed ports, and non-public destinations. - Revalidate redirect targets or disable redirects for authenticated API calls. - Separate model listing from active capability probing. - Require explicit consent before a probe that consumes quota or transmits a credential. - Retrieve only the credential for a provider already selected by the user. - Use narrowly scoped provider tokens and support secret references instead of directly reading a shared credential file. - Avoid logging full configurable URLs when they may contain sensitive query parameters. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/first_time_setup.py:68
Finding
WeChat AppSecret Stored Without Explicit Owner-Only Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/first_time_setup.py:68-77` **Vulnerability Type**: Insecure plaintext credential-file permissions **Risk Level**: Medium ### Vulnerable Code ```python @classmethod def setup_credentials(cls, app_id: str, app_secret: str): cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True) config = {} if cls.CONFIG_FILE.exists(): with open(cls.CONFIG_FILE) as f: config = json.load(f) config["app_id"] = app_id config["app_secret"] = app_secret with open(cls.CONFIG_FILE, "w") as f: json.dump(config, f, indent=2) ``` ### Technical Analysis The initial setup stores the WeChat AppSecret in plaintext at `~/.config/wechat-mp-auto/config.json`. Neither the configuration directory nor the file is assigned an explicit owner-only mode. The resulting permissions depend on the process umask and any pre-existing path permissions. Under a permissive umask, the file may be group-readable or world-readable. Opening an existing file with `"w"` also preserves its current permissions rather than tightening them. The write is not atomic and does not explicitly use no-follow semantics, which also weakens resistance to local path manipulation. ### Attack Path 1. A user runs first-time setup in an environment with permissive file-creation settings, or the configuration file already has broad permissions. 2. `setup_credentials()` writes the AppID and AppSecret to the plaintext JSON file. 3. Another local account or process with filesystem access reads the file. 4. The exposed AppSecret is used with the AppID to request WeChat access tokens. ### Impact Assessment A local attacker may obtain the long-lived WeChat AppSecret. Subject to WeChat IP allowlisting and account permissions, the attacker could request access tokens and exercise WeChat API capabilities assigned to the account, including material or draft operations. The vulnerability does not independently bypass operating-system a ...[truncated 126 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the configuration directory with mode `0700`. - Atomically create or replace the credential file with mode `0600`. - Apply `os.chmod()` to existing files after verifying that they are owned by the current user. - Reject symlinks and unexpected non-regular files before reading or writing. - Use a temporary file in the same directory, flush and synchronize it, then atomically replace the destination. - Prefer an operating-system credential store or secret manager over plaintext JSON. - Validate AppID and AppSecret formats before persistence. - Document the sensitive nature and required permissions of the file. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/token_manager.py:78
Finding
WeChat Bearer Access Token Cached Without Explicit Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `src/token_manager.py:78-95` **Vulnerability Type**: Insecure plaintext bearer-token storage **Risk Level**: Medium ### Vulnerable Code ```python def _save_to_cache(self): """保存到文件""" try: # 安全检查:确保目录存在且路径安全 self.TOKEN_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True) # 安全检查:确保文件路径安全(在预期目录内) resolved_path = self.TOKEN_CACHE_FILE.resolve() expected_dir = (Path.home() / ".cache" / "wechat-mp-auto").resolve() if not str(resolved_path).startswith(str(expected_dir)): logger.error("不安全的缓存文件路径") return data = { "app_id": self.app_id, "access_token": self._access_token, "expires_at": self._expires_at } with open(self.TOKEN_CACHE_FILE, 'w', encoding='utf-8') as f: json.dump(data, f) ``` ### Technical Analysis The cache contains an active bearer access token in plaintext. Although the code checks that the resolved path begins with the expected cache-directory string, it does not enforce owner-only permissions on the directory or token file. The file mode therefore depends on the process umask or pre-existing file permissions. A bearer token requires no additional proof of possession, so any process that can read the cache can replay it until expiration. The save is also non-atomic. The string-prefix path check is not a general safe-containment primitive, although the token path is currently a fixed class constant. ### Attack Path 1. The Skill requests a WeChat access token. 2. `_save_to_cache()` creates or overwrites `~/.cache/wechat-mp-auto/token.json`. 3. The file is created with permissions inherited from the environment or retains insecure existing permissions. 4. Another local process reads `access_token` from the JSON file. 5. The token is replayed against permitted WeChat API endpoints before it expires. ### Impact Assessment The attacker receives ...[truncated 437 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create the token cache directory with mode `0700`. - Create token files atomically with mode `0600`, using no-follow semantics where supported. - Verify that the cache file is a regular file owned by the current user before reading or replacing it. - Replace string-prefix containment checks with `Path.relative_to()` or an equivalent path-component-aware check. - Remove expired tokens from disk rather than retaining them indefinitely. - Consider an in-memory cache or operating-system credential store where persistent caching is not necessary. - Use file locking to prevent concurrent readers from observing partially written JSON. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (120)

Tainted flow: 'payload' from os.environ.get (line 144, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
"api_key": api_key
        }

        resp = requests.post(url, json=payload, headers=headers, timeout=20)
        
        if resp.status_code == 401:
            raise ValueError("Tavily API Key 无效或已过期")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tainted flow: 'payload' from os.environ.get (line 144, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Content
"api_key": api_key
        }

        resp = requests.post(url, json=payload, headers=headers, timeout=20)
        
        if resp.status_code == 401:
            raise ValueError("Tavily API Key 无效或已过期")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Credential Access

High
Category
Privilege Escalation
Content
"TAVILY_API_KEY"
    ],
    "alsoReads": [
      ".env"
    ]
  },
  "paths": {
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
"TAVILY_API_KEY"
    ],
    "alsoReads": [
      ".env"
    ]
  },
  "paths": {
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
def get_credentials(self) -> Tuple[Optional[str], Optional[str]]:
        """
        获取凭证 - 优先级:配置文件 > 环境变量 > .env
        """
        app_id = None
        app_secret = None
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
def get_credentials(self) -> Tuple[Optional[str], Optional[str]]:
        """
        获取凭证 - 优先级:配置文件 > 环境变量 > .env
        """
        app_id = None
        app_secret = None
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
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
class TokenManager:
    """Access Token 管理器"""
    
    TOKEN_CACHE_FILE = Path.home() / ".cache" / "wechat-mp-auto" / "token.json"
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The README advertises automatic draft publishing and network-based duplicate checking but does not clearly warn that article content may be transmitted to third-party services or pushed to a WeChat public account. In an AI-orchestrated skill, this is risky because users may trigger outbound data sharing or publication effects without realizing the operational consequences.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
The skill is presented entirely as a WeChat public-account article automation skill in Chinese, and the workflow/prompt conventions prescribe Chinese article structure, labels, and examples without offering any user language choice. This can violate language/locale policy when a skill mandates a specific language by default rather than allowing opt-in or clearly documenting a justified locale restriction.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill instructs sending topics, generated content, images, and publication artifacts to third-party research, image, and WeChat services without an explicit user-facing warning or consent checkpoint. This creates a real data-handling risk because sensitive or proprietary article content may be transmitted externally unexpectedly.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The workflow directs the agent to call an undefined `upload_thumb` tool, creating an inconsistency between declared and expected capabilities. In practice, this can cause runtime failure, misrouting to an unintended similarly named tool, or unsafe fallback behavior by an orchestrating agent trying to satisfy the instruction.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code reads sensitive credentials from environment variables and a local .env file, which is a safety-relevant operation under the warning criteria for code files. While there are internal log messages for successful reads, there is no docstring, comment, confirmation, or user-facing disclosure warning that the skill accesses stored secrets from these sources.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
All user-facing messages and setup instructions in this file are hardcoded in Chinese, including status and prompt text. The file does not provide any language selection, fallback, or explanation that the skill is intentionally limited to a Chinese-speaking or region-specific audience.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The code stores `app_secret` in a plaintext JSON file under the user's home directory without any protection, permission hardening, encryption, or user warning. If the local system is compromised, backups are exposed, or file permissions are too broad, an attacker could recover the WeChat AppSecret and use it to access or abuse the associated account/API integration.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The module docstring and command descriptions are entirely in Chinese, and the tool presents its interface and status messaging in that locale only. This is a language/locale restriction in natural-language UX with no opt-in or alternative language path documented.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script automatically downloads attacker-controlled external image URLs and re-uploads them using the publisher's WeChat credentials, without explicit consent or any allowlist. This creates unannounced outbound network access, can leak operational metadata/IP, and can be abused to make the system interact with arbitrary remote hosts or relay untrusted content through the publisher account.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
The method sends analytics query data to a remote endpoint via `self.post(...)`, and similar behavior appears again for user statistics retrieval. In this file there is no confirmation prompt, logging/print statement, or explanatory comment/docstring warning that article and user analytics data will be transmitted to an external API.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The call to `self.post("/cgi-bin/analysis/get_user_summary", data)` fetches user-summary analytics, which can be privacy-sensitive. This file does not include any visible warning, confirmation, or explanatory documentation telling the user that account/user analytics will be requested from a remote service.

Natural-Language Policy Violations

Medium
Confidence
90% confidence
Finding
This code file contains user-facing natural-language descriptions, docstrings, log messages, and error text entirely in Chinese, such as the module description and method documentation. Under the policy rule for language/locale, forcing a specific language without user opt-in or a documented regional justification is a natural-language policy concern.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
Natural-language strings, documentation, and user-facing messages in this file are exclusively in Chinese, with no indication that users may opt into another language or locale. Under the stated policy, forcing a specific language without user choice can be a policy violation unless clearly justified as region-specific.

Static analysis

No suspicious patterns detected.