Back to skill

Security audit

Dingding

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly a DingTalk notification helper, but its script can send file contents to any URL supplied in the environment despite documenting DingTalk-only network access.

Install only if you trust the environment that sets DING_WEBHOOK and the agents or automations that invoke the CLI. Treat markdown file sends as capable of transmitting any readable text file, and prefer adding URL validation for https://oapi.dingtalk.com plus explicit user confirmation before sending local files or using @all.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/dingbot.py:37
Finding
Unrestricted Webhook Destination Enables Disclosure of Local File Contents<![CDATA[ ## Vulnerability Details **File Location**: `scripts/dingbot.py:37-53`, `scripts/dingbot.py:60-61`, and `scripts/dingbot.py:88-91` **Vulnerability Type**: Unvalidated outbound destination combined with arbitrary local file transmission **Risk Level**: Medium ### Vulnerable Code ```python def signed_url() -> str: url = os.environ.get("DING_WEBHOOK") or die("未设置 DING_WEBHOOK") secret = os.environ.get("DING_SECRET") if not secret: return url ts = str(round(time.time() * 1000)) string_to_sign = f"{ts}\n{secret}".encode("utf-8") sign = base64.b64encode( hmac.new(secret.encode("utf-8"), string_to_sign, digestmod=hashlib.sha256).digest()) return f"{url}&timestamp={ts}&sign={urllib.parse.quote_plus(sign)}" def send(body: dict) -> None: req = urllib.request.Request( signed_url(), json.dumps(body, ensure_ascii=False).encode("utf-8"), {"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=20) as resp: r = json.loads(resp.read()) ``` ```python def read_arg_or_file(v: str) -> str: return open(v, encoding="utf-8").read() if os.path.isfile(v) else v ``` ```python elif cmd == "markdown": len(rest) >= 2 or die("用法: markdown <标题> <md文件或内容>") send({"msgtype": "markdown", "markdown": {"title": rest[0], "text": read_arg_or_file(rest[1])}}) ``` ### Technical Analysis The documentation states that network requests are limited to `oapi.dingtalk.com`, but the implementation obtains the complete destination from the `DING_WEBHOOK` environment variable and does not validate its scheme, hostname, port, user-information component, or resolved address. The Markdown command treats its second argument as a local path whenever `os.path.isfile()` succeeds. It reads the entire file and places its contents in the outbound JSON message. This is expected when deliberately sending a report file to DingTalk, but the absenc ...[truncated 2587 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `DING_WEBHOOK` with `urllib.parse.urlsplit()` before creating the request. 2. Require the `https` scheme and require the normalized hostname to be exactly `oapi.dingtalk.com`. Do not use suffix-only checks that could accept domains such as `oapi.dingtalk.com.attacker.example`. 3. Reject embedded usernames or passwords, fragments, malformed URLs, and unexpected ports. 4. Use a redirect handler that either disables redirects or validates every redirect target against the same HTTPS and hostname allowlist. 5. Consider resolving the hostname and rejecting loopback, link-local, private, and reserved destinations if future configuration permits more than one approved hostname. 6. Restrict file-based Markdown input to an explicitly approved workspace or report directory. Resolve paths with `realpath()` and verify that the resulting path remains under that directory. 7. Consider separating literal Markdown content from file input into distinct options, such as `--content` and `--file`, to prevent an argument from unexpectedly being interpreted as a file path. 8. Apply a reasonable file-size limit and reject non-regular files before reading them. 9. Add automated tests covering malicious hostnames, alternate schemes, embedded credentials, nonstandard ports, redirects to unapproved hosts, path traversal, and symbolic links escaping an approved directory. ]]>
Vulnerability Patterns
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
代码与声明中的“群机器人 CLI”部分高度一致:确实支持 webhook 发送、加签安全模式、text/markdown/link 三种消息,并支持 @all/指定手机号。但声明还包含更广泛的‘钉钉开放平台 API(token/审批/通讯录)开发指导’能力,而给出的代码片段完全没有实现这些 API 调用、认证流程或开发指导逻辑。按标准,若声明的主要能力与实际代码覆盖范围存在明显差异,应判定为不匹配。因此这是部分匹配但整体仍存在描述与行为不一致。

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill documents use of environment variables and outbound network access but does not declare any tool scope or permissions boundaries. In an agent ecosystem, this weakens policy enforcement and transparency, making it easier for a caller to invoke networked behavior or consume secrets from the environment without explicit review.

Static analysis

No suspicious patterns detected.