Back to skill

Security audit

Dingtalk

Security checks for vulnerabilities and agentic risk

Overview

This DingTalk integration is purpose-aligned, but it accepts inbound messages without authenticating DingTalk callbacks and allows arbitrary webhook destinations.

Review before installing. Use only tightly scoped DingTalk credentials, prefer environment variables or protected secret storage, and do not expose the callback endpoint unless callback signature verification and sender/group allowlist enforcement are fixed. Avoid configuring webhook URLs outside the official DingTalk robot endpoint.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
index.ts:199
Finding
Inbound DingTalk callbacks are accepted without authentication or access-policy enforcement<![CDATA[ ## Vulnerability Details **File Location**: `index.ts:199-218` **Vulnerability Type**: Missing callback authentication and authorization **Risk Level**: High ### Vulnerable Code ```typescript async receive( ctx: ChannelPluginContext<DingTalkConfig>, payload: unknown ): Promise<InboundMessage | null> { // Handle incoming webhook from DingTalk const data = payload as Record<string, unknown>; // DingTalk callback format if (data.msgtype === "text" && data.text) { const textData = data.text as { content?: string }; return { id: String(data.msgId || Date.now()), channel: "dingtalk", content: { type: "text", text: textData.content || "" }, authorId: String(data.senderStaffId || data.staffId || "unknown"), authorName: String(data.senderNick || "Unknown"), conversationId: String(data.conversationId || data.chatId || "private"), timestamp: new Date(Number(data.createTime) || Date.now()), }; } return null; } ``` ### Technical Analysis The callback handler casts an untrusted payload to a record and creates an `InboundMessage` based solely on attacker-controlled fields. It does not verify a DingTalk callback signature, timestamp, nonce, encryption wrapper, or replay status. The configuration declares `encryptKey`, `dmPolicy`, `allowFrom`, `groupPolicy`, and `groupAllowFrom`, but this handler does not use any of them. Consequently, a payload can claim an arbitrary `senderStaffId`, `staffId`, `conversationId`, or `chatId`, and the configured allowlists do not provide effective protection within the audited implementation. The repository includes a `DingTalkCallbackPayload` type containing `encrypt`, `msg_signature`, `timestamp`, and `nonce`, but the callback implementation does not process those security fields. ### Attack Path 1. An attacker identifies or gains network access to the OpenClaw DingTalk callback route. 2. The attacker submits a forged payload such as a text message with an ...[truncated 1021 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate every inbound callback using DingTalk's documented signature-verification procedure before parsing message content. 2. Verify the timestamp and nonce and reject stale callbacks. 3. Maintain a bounded replay cache for recently accepted message IDs, signatures, or nonces. 4. Use constant-time comparison for authentication values. 5. Decrypt encrypted callback envelopes using the configured encryption key where the DingTalk integration requires encryption. 6. Reject unsigned or malformed payloads by default rather than falling back to permissive parsing. 7. Determine whether the event is a direct or group message and enforce: - `dmPolicy` and `allowFrom` for direct messages. - `groupPolicy` and `groupAllowFrom` for group messages. 8. Do not trust sender or conversation identifiers until callback authenticity has been established. 9. Add tests for forged signatures, stale timestamps, replayed messages, spoofed sender IDs, and allowlist bypass attempts. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
index.ts:107
Finding
Unrestricted webhook URL permits server-side request forgery and outbound message disclosure<![CDATA[ ## Vulnerability Details **File Location**: `index.ts:107-133`; configuration source at `onboarding.ts:291-314` **Vulnerability Type**: Unvalidated outbound request destination **Risk Level**: Medium ### Vulnerable Code ```typescript async function sendWebhookMessage( webhookUrl: string, secret: string | undefined, content: string ): Promise<boolean> { try { let url = webhookUrl; // Add signature if secret is provided if (secret) { const timestamp = Date.now(); const signature = await generateSignature(timestamp, secret); url = `${webhookUrl}&timestamp=${timestamp}&sign=${signature}`; } const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ msgtype: "text", text: { content }, }), }); const data = await response.json(); return data.errcode === 0 || data.errmsg === "ok"; } catch (error) { console.error("Failed to send DingTalk webhook message:", error); return false; } } ``` The onboarding flow accepts the destination without validation: ```typescript if (useWebhook) { const webhookUrl = await prompter.text({ message: "Webhook URL", placeholder: "https://oapi.dingtalk.com/robot/send?access_token=xxxxx", }); const webhookSecret = await prompter.text({ message: "Webhook Secret (for signature verification, optional)", placeholder: "SECxxxxx", }); next = { ...next, channels: { ...next.channels, dingtalk: { ...next.channels?.dingtalk, webhookUrl: webhookUrl || undefined, webhookSecret: webhookSecret || undefined, }, }, }; } ``` ### Technical Analysis Although the declared feature is a DingTalk robot webhook, the implementation accepts and fetches any configured URL. It does not restrict the scheme, hostname, port, path, resolved IP address, or redirect destination. Outbound message content is ...[truncated 1806 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse the URL with a strict URL parser and require HTTPS. 2. Allowlist the documented DingTalk webhook hostname, such as the exact expected `oapi.dingtalk.com` host, and validate the expected robot webhook path. 3. Reject user-information components, unexpected ports, malformed query strings, and non-DingTalk destinations. 4. Resolve the hostname and reject loopback, private, link-local, multicast, reserved, and cloud metadata IP ranges for both IPv4 and IPv6. 5. Disable redirects or validate every redirect destination against the same policy. 6. Protect against DNS rebinding by validating the address actually used for the connection. 7. Treat webhook URLs as secrets because they contain access tokens; redact them from logs and restrict configuration-file permissions. 8. Consider storing only a DingTalk webhook token rather than accepting a complete URL, then construct the trusted endpoint internally. 9. Validate the response status and content type before parsing JSON, and apply short connection and response timeouts. ]]>

T05 · Unauthorized Access and Privilege Escalation

Note
Location
probe.ts:33
Finding
Credential probe unnecessarily enumerates account micro-app metadata<![CDATA[ ## Vulnerability Details **File Location**: `probe.ts:33-52` **Vulnerability Type**: Excessive data access during credential validation **Risk Level**: Low ### Vulnerable Code ```typescript // Try to get bot/app info try { const appInfoResponse = await fetch( `https://oapi.dingtalk.com/topapi/microapp/list?access_token=${accessToken}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({}), } ); const appInfo = await appInfoResponse.json(); if (appInfo.errcode === 0 && appInfo.appList && appInfo.appList.length > 0) { const app = appInfo.appList[0]; return { ok: true, botName: app.name, botUserId: app.agentId, }; } } catch { // Ignore app info errors } ``` ### Technical Analysis The probe has already established credential validity by obtaining an access token. It then calls the account-wide `microapp/list` endpoint and processes the first visible application. Enumerating the application list is broader than the minimum operation required for a connection test. It may retrieve metadata for unrelated applications, and selecting the first entry does not establish that the application corresponds to the configured DingTalk robot or client. The request is sent to the official DingTalk host; the issue is unnecessary privilege and data use, not transmission to an unknown hard-coded third party. ### Attack Path 1. A caller supplies valid DingTalk credentials to `probeDingTalk()`. 2. The function obtains an access token from the official DingTalk endpoint. 3. It uses that token to request all micro-apps visible to the credential. 4. It returns the name and agent ID of the first application to the probe caller, even if that application is unrelated to the configured integration. ### Impact Assessment The probe can expose limited organization application metadata beyond what is required to establish connectivity. It may also encourage granting a ...[truncated 302 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. End the connectivity probe after successful token acquisition if only credential validity is required. 2. If application identity must be displayed, query only an explicitly configured `robotCode`, agent ID, or application identifier. 3. Document why application metadata is needed and request only the corresponding minimum DingTalk API scope. 4. Do not select the first application from an account-wide list as the configured bot identity. 5. Return only the minimum metadata required by the onboarding interface. 6. Add tests confirming that a basic credential probe does not enumerate unrelated account resources. ]]>
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
  • Rogue AgentSelf-Modification, Session Persistence
Findings (13)

Credential Access

High
Category
Privilege Escalation
Content
return { clientId, clientSecret };
}

// Get access token from DingTalk
async function getAccessToken(
  clientId: string,
  clientSecret: string
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
return { clientId, clientSecret };
}

// Get access token from DingTalk
async function getAccessToken(
  clientId: string,
  clientSecret: string
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
return { clientId, clientSecret };
}

// Get access token from DingTalk
async function getAccessToken(
  clientId: string,
  clientSecret: string
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill documents capabilities that inherently use environment variables and network access, but it does not declare any explicit tool scope such as permissions or allowed-tools. In an agent ecosystem, missing scope declarations can cause overbroad execution or prevent users and reviewers from understanding that the skill can access secrets and communicate externally, increasing the risk of unintended data exposure or misuse.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The skill includes multiple examples for supplying client secrets and webhook tokens, but it provides no warning about secret handling, redaction, repository exposure, shell history leakage, or least-privilege storage. Because this is a messaging integration skill dealing with enterprise credentials, users may copy secrets into config files or logs insecurely, leading to account compromise or unauthorized message sending.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
This code sends DingTalk credentials over HTTP requests to obtain an access token, and later transmits message content to DingTalk APIs and webhooks. While there is internal logging for failures, there is no user-facing warning, confirmation, or explanatory comment/docstring disclosing that user/system data and configured secrets will be sent to an external service.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The plugin will POST message content to any configured webhookUrl without restricting the destination to DingTalk-owned domains. In a channel plugin, this creates an SSRF/exfiltration primitive: anyone who can influence configuration can redirect outbound messages and, when present, the webhook signature parameters to an arbitrary external endpoint.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The onboarding flow explicitly prompts for a DingTalk client secret and webhook secret, then writes them into the persistent channel configuration object. Storing long-lived secrets in config increases exposure through logs, config exports, backups, source control mistakes, or broader read access than intended, especially since the flow presents this as normal without warning users to prefer environment variables or secret storage.

Missing User Warnings

Medium
Confidence
85% confidence
Finding
This code sends the configured DingTalk client ID and client secret to a remote API as part of an HTTP request, but the file contains no confirmation prompt, logging, comment-based warning, or other user-facing disclosure about that sensitive network operation. Because transmitting credentials is a safety-relevant action for code files, the absence of any warning meets the missing-user-warning criterion.

Session Persistence

Medium
Category
Rogue Agent
Content
);

      const appInfo = await appInfoResponse.json();
      if (appInfo.errcode === 0 && appInfo.appList && appInfo.appList.length > 0) {
        const app = appInfo.appList[0];
        return {
          ok: true,
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
);

      const appInfo = await appInfoResponse.json();
      if (appInfo.errcode === 0 && appInfo.appList && appInfo.appList.length > 0) {
        const app = appInfo.appList[0];
        return {
          ok: true,
Confidence
75% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Vague Triggers

Low
Confidence
78% confidence
Finding
The manifest lists the alias "钉钉" as a trigger-like identifier, but it does not provide any scope constraints or exclusion conditions. As a short everyday product name, it may be invoked in general conversation about the app rather than as an explicit request to use this skill.

Unverifiable Dependency: openclaw has 16 known advisory(ies) (CVE-2026-53846 (OpenClaw: Workspace .env npm_execpath could influence bundled runtime dependency); CVE-2026-32064 (OpenClaw's andbox browser noVNC observer lacked VNC authentication); CVE-2026-32006 (OpenClaw has a BlueBubbles group allowlist mismatch via DM pairing-store fallbac) +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
40% confidence
Finding
Dependency has known vulnerabilities (CVEs). Using packages with unpatched security flaws exposes the environment to known exploits.

Static analysis

Detected: suspicious.env_credential_access, suspicious.exposed_secret_literal

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
index.ts:43

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
index.ts:44

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
onboarding.ts:225

File appears to expose a hardcoded API secret or token.

Critical
Code
suspicious.exposed_secret_literal
Location
probe.ts:32