Back to skill

Security audit

OpenClaw Feishu Group Chat

Security checks for vulnerabilities and agentic risk

Overview

The skill is disclosed as a Feishu chat helper, but it asks admins to persist the whole Feishu contact directory into agent prompt context, which needs careful review before installation.

Install only with administrator approval for Feishu contact access. Treat names and open_ids as personal data, keep USER.md private, avoid the weekly cron until scoping and retention are decided, and prefer a restricted lookup or allowlisted sync with escaping/validation before storing any directory data in prompt context.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • 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
  • 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)

T02 · Agent Memory Poisoning

Warning
Location
scripts/sync_feishu_contacts.py:62
Finding
Untrusted Feishu Directory Data Is Persisted in Agent Prompt Context<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_feishu_contacts.py`, lines 62-79 and 100-101 **Vulnerability Type**: Persistent indirect prompt injection through unsanitized contact data **Risk Level**: Medium ### Vulnerable Code ```python for u in data.get("items", []): name = u.get("name", "") open_id = u.get("open_id", "") if name and open_id: users.append({"name": name, "open_id": open_id}) if not data.get("has_more"): break page_token = data.get("page_token") if not users: print("Warning: no users found. Check app permissions (need contact:user.base:readonly).") sys.exit(1) # 4. Update USER.md contacts table TABLE_HEADER = "| 姓名 | open_id |\n|------|---------|" table_rows = "\n".join(f"| {u['name']} | {u['open_id']} |" for u in users) ``` The resulting content is subsequently persisted: ```python with open(user_md_path, "w") as f: f.write(new_content) ``` `SKILL.md`, lines 15-17, establishes that this file is injected into the Agent's system prompt: ```markdown **Fix**: Embed an `open_id → name` lookup table in USER.md. Since workspace files are injected into the system prompt, the agent matches senders instantly — no tool calls. ``` ### Technical Analysis Feishu-provided `name` and `open_id` values are inserted directly into a Markdown table without validation or escaping. In particular, the implementation does not reject newlines, Markdown table separators, control characters, oversized values, or instruction-like content. Because `USER.md` is intended to be loaded into the Agent's persistent prompt context, directory data crosses a trust boundary: externally controlled profile information is converted into persistent high-trust Agent context. If a directory user can control a display name containing formatting or instruction text, the value may break out of the intended table row and introduce content that the Agent could interpret as instructions. This is a persistent indirect prompt- ...[truncated 1897 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate directory fields before storing them: - Reject newline, carriage-return, null, and other control characters. - Enforce conservative maximum lengths. - Validate `open_id` against the expected Feishu identifier format. 2. Escape Markdown metacharacters, especially pipe characters and backslashes, before generating table rows. 3. Treat contact records explicitly as untrusted data rather than instructions. 4. Prefer storing the mapping in a structured data file, such as JSON, outside system-prompt context. 5. Expose a narrowly scoped contact-lookup function that returns only the matching record when required. 6. If prompt storage remains necessary, place the data inside a clearly delimited section accompanied by an instruction that its contents are untrusted data and must never be followed as instructions. 7. Review existing generated `USER.md` files for unexpected multiline entries and reload the gateway only after sanitizing them. 8. Add tests using names containing newlines, pipes, backticks, headings, and instruction-like text to verify that table breakout is impossible. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/sync_feishu_contacts.py:52
Finding
Organization-Wide Contact Collection Exceeds the Data Needed for Sender Identification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/sync_feishu_contacts.py`, lines 52-67 and 76-101 **Vulnerability Type**: Excessive collection and prompt-context exposure of personal directory data **Risk Level**: Medium ### Vulnerable Code The script enumerates the root department and follows pagination, resulting in collection of the full directory visible to the application: ```python # 3. Fetch all users (paginated) users = [] page_token = None while True: url = "https://open.feishu.cn/open-apis/contact/v3/users/find_by_department?department_id=0&page_size=50" if page_token: url += f"&page_token={page_token}" req = urllib.request.Request(url, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json" }) data = json.loads(urllib.request.urlopen(req).read()).get("data", {}) for u in data.get("items", []): name = u.get("name", "") open_id = u.get("open_id", "") if name and open_id: users.append({"name": name, "open_id": open_id}) if not data.get("has_more"): break page_token = data.get("page_token") ``` All collected records are rendered and written to `USER.md`: ```python TABLE_HEADER = "| 姓名 | open_id |\n|------|---------|" table_rows = "\n".join(f"| {u['name']} | {u['open_id']} |" for u in users) with open(user_md_path, "r") as f: content = f.read() pattern = r"(## 飞书通讯录[^\n]*\n[^\n]*\n)\| 姓名 \| open_id \|\n\|[-| ]*\n((\|[^\n]*\n)*)" new_table = f"\\1{TABLE_HEADER}\n{table_rows}\n" new_content = re.sub(pattern, new_table, content) if new_content == content: if "## 飞书通讯录" not in content: print(f"Error: USER.md has no '## 飞书通讯录' section. Add it first:") print(f" ## 飞书通讯录 ({app_name} App)") print(f" 飞书 DM 不携带发送者姓名。用 inbound metadata 的 chat_id(格式 `user:ou_xxx`)匹配下表识别发送者。") print(f" | 姓名 | open_id |") print(f" |------|---------|") sys.exit(1) print(f"OK: {l ...[truncated 3022 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply data minimization: - Synchronize only an explicitly allowlisted department or user set. - Store records only for users who have interacted with the Agent. - Retrieve an individual contact on demand when a direct message is received. 2. Keep the contact mapping outside `USER.md` and system-prompt context. 3. Implement a scoped lookup tool that accepts an `open_id` and returns only the corresponding display name. 4. Request the narrowest Feishu contact permissions compatible with the selected lookup design. 5. Create output files with restrictive permissions, such as owner read/write only, and verify permissions before updating an existing file. 6. Document names and stable account identifiers accurately as personal data. 7. State clearly where the directory is stored, when it is loaded into model context, how long it is retained, and how it can be deleted. 8. Avoid including full contact mappings in logs, backups, diagnostic output, or model prompts. 9. Provide an opt-in synchronization scope and require administrators to acknowledge organization-wide collection before enabling it. 10. Establish retention and deletion procedures for generated contact data and remove stale users during synchronization. ]]>
Vulnerability Patterns
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding
The published purpose is a behavioral/group-chat skill, but the content also instructs operators to fetch an org contact directory from a remote API, use app credentials, and write synchronized identity data into USER.md. This mismatch is dangerous because reviewers or users may enable the skill expecting only conversational behavior changes, while it actually introduces data collection, credential use, and persistent file modification affecting privacy and trust boundaries.

Credential Access

High
Category
Privilege Escalation
Content
print(f"Available accounts: {list(cfg.get('channels', {}).get('feishu', {}).get('accounts', {}).keys())}")
        sys.exit(1)

    # 2. Get tenant access token
    req = urllib.request.Request(
        "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
        data=json.dumps({
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
93% confidence
Finding
The skill describes and depends on capabilities equivalent to network access and file reads/writes, but it does not declare an explicit tool scope or permissions boundary. That makes the skill harder to review and govern, and in a real agent environment can lead to over-privileged execution or unintended access if the runtime grants broad default capabilities.

Natural-Language Policy Violations

Medium
Confidence
87% confidence
Finding
The document instructs users to populate USER.md with a Chinese-language heading and explanatory text, and later provides a Chinese-only startup snippet. This imposes a specific language on the skill configuration without offering a language choice or documenting that the skill is intentionally limited to Chinese-speaking deployments.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The AGENTS.md startup sequence is specified entirely in Chinese, which can require a specific locale/language for operation or maintenance. The file does not offer an alternative language version or indicate that this language constraint is optional or justified.

Description-Behavior Mismatch

Medium
Confidence
90% confidence
Finding
The script exports the full Feishu contact directory, including names and open_id values, into USER.md, which may be broadly accessible in a workspace or repository. This creates an unnecessary bulk copy of directory data and increases the risk of privacy leakage, internal user enumeration, and secondary misuse beyond the narrow need of identifying DM senders.

Tainted flow: 'req' from urllib.request.urlopen (line 58, network input) → urllib.request.urlopen (network output)

Medium
Category
Data Flow
Content
}).encode(),
        headers={"Content-Type": "application/json"}
    )
    resp = json.loads(urllib.request.urlopen(req).read())
    if resp.get("code", 0) != 0:
        print(f"Error getting token: {resp.get('msg', 'unknown error')}")
        sys.exit(1)
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Context-Inappropriate Capability

Medium
Confidence
86% confidence
Finding
Bulk synchronization of all users is not clearly necessary for a skill whose stated purpose is group-chat behavior and sender recognition. In this context, collecting and persisting the entire directory broadens exposure of personal data and makes the skill more dangerous because it turns a chat-integration helper into a directory replication mechanism.

Tainted flow: 'req' from urllib.request.urlopen (line 58, network input) → urllib.request.urlopen (network output)

Medium
Category
Data Flow
Content
"Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        })
        data = json.loads(urllib.request.urlopen(req).read()).get("data", {})
        for u in data.get("items", []):
            name = u.get("name", "")
            open_id = u.get("open_id", "")
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Static analysis

No suspicious patterns detected.