Back to skill

Security audit

qywx-notify

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it claims, but it can expose sensitive webhook credentials and can be pointed at arbitrary URLs from the host running it.

Review before installing. Use only in a trusted, single-user context unless the webhook handling is fixed. Do not put real WeCom webhook URLs into this version if logs or command outputs are visible to others, and restrict or remove caller-supplied webhook URLs so the skill cannot be used as a general network request proxy. Pin dependencies and rotate any webhook that may already have appeared in logs.

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

T09 · Insecure Skill Coding Practices

Warning
Location
index.js:43
Finding
Webhook credentials are exposed through logs and returned objects<![CDATA[ ## Vulnerability Details **File Location**: `index.js:26-43`, `index.js:94-102`, and `index.js:263-269` **Vulnerability Type**: Sensitive credential exposure **Risk Level**: Medium ### Technical Analysis The webhook URL contains a bot token and is explicitly identified as sensitive in `SKILL.md`. Although `_maskWebhook()` exists, several code paths expose the unmasked URL. The constructor logs the complete configuration, including `defaultWebhook`: ```javascript this.config = { enabled: config.enabled !== false, defaultWebhook: config.defaultWebhook || '', timeout: config.timeout || 10000, retryCount: config.retryCount || 3, retryDelay: config.retryDelay || 1000, ...config }; // HTTP client this.httpClient = axios.create({ timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'User-Agent': 'OpenClaw-Qywx-Notify/1.0.0' } }); this.log(`Skill initialized with config: ${JSON.stringify(this.config, null, 2)}`); ``` When message delivery fails, `send()` returns the original parameter object. If the caller supplied a webhook through `params.webhook`, its token is returned without masking: ```javascript } catch (error) { this.error('Failed to send notification:', error.message); return { success: false, message: `Send failed: ${error.message}`, error: error.response?.data || error.message, request: params }; } ``` The `config` command similarly returns the entire unredacted configuration even though it also provides a separately masked value: ```javascript case 'config': return { success: true, config: this.config, maskedWebhook: this.config.defaultWebhook ? this._maskWebhook(this.config.defaultWebhook) : null }; ``` A WeCom webhook URL is a bearer credential: possession of the URL and embedded bot token may be sufficient to submit messages as the configured bot. Masking only the separately generated `maskedWebhook` field does not protect the raw value still pre ...[truncated 1457 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Never serialize the full configuration object. Log only an explicit allowlist of non-sensitive fields: ```javascript this.log('Skill initialized', { enabled: this.config.enabled, timeout: this.config.timeout, retryCount: this.config.retryCount, hasDefaultWebhook: Boolean(this.config.defaultWebhook) }); ``` 2. Remove `config: this.config` from the `config` command. Return a sanitized object instead: ```javascript case 'config': return { success: true, config: { enabled: this.config.enabled, timeout: this.config.timeout, retryCount: this.config.retryCount, retryDelay: this.config.retryDelay, hasDefaultWebhook: Boolean(this.config.defaultWebhook) }, maskedWebhook: this.config.defaultWebhook ? this._maskWebhook(this.config.defaultWebhook) : null }; ``` 3. Sanitize failed request information before returning it: ```javascript request: { ...params, webhook: params.webhook ? this._maskWebhook(params.webhook) : undefined } ``` Prefer returning only fields that are necessary for troubleshooting. 4. Apply centralized redaction to URLs, authorization values, tokens, and configuration fields before logging or sending telemetry. 5. Restrict access to existing logs and delete retained entries containing webhook URLs where operationally feasible. 6. Rotate any webhook credential that may already have appeared in logs or command responses. 7. Add tests asserting that complete webhook tokens never occur in logs, successful responses, failed responses, or configuration output. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
index.js:213
Finding
Unrestricted webhook URLs enable server-side request forgery<![CDATA[ ## Vulnerability Details **File Location**: `index.js:108-117`, `index.js:163-176`, and `index.js:213-221` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: Medium ### Technical Analysis The skill accepts a caller-controlled `webhook` and validates only whether JavaScript's `URL` constructor can parse it: ```javascript const webhook = params.webhook || this.config.defaultWebhook; if (!webhook) { throw new Error('Missing Webhook URL. Please provide the webhook parameter or configure defaultWebhook.'); } if (!params.content || params.content.trim() === '') { throw new Error('Notification content cannot be empty.'); } // Validate Webhook URL format if (!this._isValidUrl(webhook)) { throw new Error(`Invalid Webhook URL: ${webhook}`); } ``` The validation method does not restrict the protocol, hostname, port, path, DNS resolution, redirects, or destination network: ```javascript _isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } ``` The accepted value is then used directly as the destination of a server-side POST request: ```javascript async _sendRequest(webhook, data, retry = 0) { try { this.log(`Sending request to: ${this._maskWebhook(webhook)}`); this.log(`Request data: ${JSON.stringify(data, null, 2)}`); const response = await this.httpClient.post(webhook, data); ``` Consequently, a caller able to invoke the skill can direct the OpenClaw host to make HTTP requests to arbitrary reachable services rather than only approved WeCom webhook endpoints. This includes loopback addresses, private network ranges, internal DNS names, link-local services, and arbitrary external collection servers. The response body is returned to the caller through `send()` as `data: result.data`, which can expose responses from reachable internal services. Axios also follows supported redirects by default, so validating only the original textual URL would remain insufficient ev ...[truncated 1869 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not accept arbitrary webhook destinations when a fixed configured endpoint is sufficient. Prefer storing the approved webhook in protected configuration and disallow per-request overrides. 2. Enforce HTTPS explicitly: ```javascript const parsed = new URL(webhook); if (parsed.protocol !== 'https:') { throw new Error('Only HTTPS webhook URLs are allowed.'); } ``` 3. Maintain an explicit allowlist of legitimate WeCom webhook hostnames and required path prefixes. Do not rely on substring or suffix checks that permit domains such as `trusted.example.attacker.com`. 4. Reject URLs containing embedded credentials, unexpected ports, fragments, or malformed hostnames. 5. Resolve the hostname and reject every resulting loopback, private, link-local, multicast, reserved, and unspecified IP address for both IPv4 and IPv6. Revalidate the actual destination at connection time to mitigate DNS rebinding and time-of-check/time-of-use issues. 6. Disable redirects, or validate every redirect target with the same protocol, hostname, DNS, and IP controls: ```javascript this.httpClient = axios.create({ timeout: this.config.timeout, maxRedirects: 0, headers: { 'Content-Type': 'application/json', 'User-Agent': 'OpenClaw-Qywx-Notify/1.0.0' } }); ``` 7. Apply outbound firewall or proxy policy so the process can connect only to approved webhook destinations. Application validation should not be the only SSRF defense. 8. Limit response size and avoid returning arbitrary remote response bodies to callers. Return only the WeCom fields needed by the integration. 9. Add tests covering loopback, RFC1918, link-local, IPv6 local ranges, integer or encoded IP representations, internal DNS, non-HTTPS schemes, unexpected ports, DNS rebinding, and redirect chains. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (6)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
The primary purpose matches the description: this is a WeChat Work/WeCom notification sender. However, the declared permissions are empty, while the code performs outbound HTTP POST requests to provided or configured webhook URLs using axios. That network capability is material and should be declared. In addition, the skill exposes extra operational capabilities beyond simple notification sending: a `test` command that sends a live test notification and a `config` command that returns configuration details. These are related to the skill but are not reflected in the description. No evidence of unrelated data exfiltration or a materially different purpose was found.

Context-Inappropriate Capability

Medium
Confidence
99% confidence
Finding
Initialization logging serializes and prints the entire runtime configuration, which includes the `defaultWebhook` secret. Logs are commonly accessible to operators, support tooling, or aggregated log platforms, so this creates a durable secret leak that can enable unauthorized message sending if the webhook is recovered.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The skill logs the full default webhook URL without warning or redaction, exposing a credential-like secret in plaintext. In the context of a notification skill, the webhook is the primary authorization mechanism, so leaking it directly undermines access control to the downstream WeCom group bot.

Context-Inappropriate Capability

Medium
Confidence
97% confidence
Finding
The `config` command returns the full `this.config` object, which includes `defaultWebhook` in plaintext. A WeCom webhook URL is effectively a secret because anyone who obtains it can send arbitrary messages to the target group, so exposing it through an operational command is an unnecessary secret-disclosure path for this skill.

Unpinned Dependencies

Low
Category
Supply Chain
Content
}
  },
  "dependencies": {
    "axios": "^1.6.0"
  },
  "engines": {
    "node": ">=16.0.0"
Confidence
95% confidence
Finding
The dependency is specified with a caret range (^1.6.0), which allows different installed versions over time instead of a fully fixed release. This weakens supply-chain reproducibility and can silently introduce vulnerable or behavior-changing versions during installation, especially in a notification skill that makes outbound network requests.

Unverifiable Dependency: axios has 16 known advisory(ies) (CVE-2026-44494 (axios Vulnerable to Full Man-in-the-Middle via Prototype Pollution Gadget in `co); CVE-2026-44495 (axios Vulnerable to Credential Theft and Response Hijacking via Prototype Pollut); CVE-2025-62718 (Axios has a NO_PROXY Hostname Normalization Bypass that Leads to SSRF) +13 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
78% confidence
Finding
The manifest depends on axios without an exact pinned version, and axios has a history of advisories including SSRF- and proxy-related issues. Because this skill sends notifications via outbound HTTP requests, any affected axios release could be security-relevant in this context, and the non-pinned range makes it impossible to verify from the manifest alone whether deployments are safe.

Static analysis

No suspicious patterns detected.