Back to skill

Security audit

openclaw-bottle-drift-skill

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent messaging skill, but network/shared use can expose private messages and reply links because the relay has no real user authentication.

Install only if you will run this as a local/demo relay or behind your own authentication, HTTPS, and access controls. Do not expose it publicly as-is: anyone who can reach the relay may be able to view another user's inbox, impersonate users by choosing their IDs, and collect reply links or callback URLs.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/relay_server.py:578
Finding

Unauthenticated Inbox Access Exposes Private Messages and Reply Tokens

Content
View full analysis
Dict[str, Any]: received_rows = self.conn.execute( """ SELECT d.delivery_id, d.reply_token, d.delivered_at, d.expires_at, b.bottle_id, b.sender_id, b.sender_name, b.message, b.created_at, EXISTS( SELECT 1 FROM replies r WHERE r.delivery_id = d.delivery_id ) AS has_replied FROM deliveries d JOIN bottles b ON b.bottle_id = d.bottle_id WHERE d.recipient_id=? ORDER BY b.created_at DESC """, (user_id,), ).fetchall() sent_rows = self.conn.execute( """ SELECT bottle_id, sender_id, sender_name, message, created_at, expires_at, fanout FROM bottles WHERE sender_id=? ORDER BY created_at DESC """, (user_id,), ).fetchall() replies_rows = self.conn.execute( """ SELECT reply_id, bottle_id, recipient_id, replier_name, reply_text, created_at FROM replies WHERE sender_id=? ORDER BY created_at DESC """, (user_id,), ).fetchall() received_bottles = [] for row in received_rows: item = dict(row) item["created_at_text"] = iso_time(int(item["created_at"])) item["delivered_at_text"] = iso_time(int(item["delivered_at"])) item["expires_at_text"] = iso_time(int(item["expires_at"])) item["reply_url"] = f"{self.base_url}/r/{item['reply_token']}" item["has_replied"] = bool(item["has_replied"]) received_bottles.append(item) sent_bottles = [] for row in sent_rows: item = dict(ro ...[truncated 3823 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/relay_server.py:590
Finding

Client-Controlled Identities Permit Impersonation and Rate-Limit Bypass

Content
View full analysis
= SEND_RATE_LIMIT_PER_MINUTE: raise ValueError("send rate limit exceeded; try again later") ``` ### Technical Analysis The relay accepts `user_id` and `sender_id` directly from unauthenticated request bodies. Input validation restricts the characters and length of an identifier, but it does not prove that the caller owns that identity. The heartbeat operation uses an upsert keyed by `user_id`, allowing any caller to overwrite an existing user’s display name, callback URL, receiving preference, and last-seen sta ...[truncated 1382 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/relay_server.py:253
Finding

Online-User Endpoint Publicly Discloses Stored Callback URLs

Content
View full analysis
List[Dict[str, Any]]: cutoff = now_ts() - ONLINE_WINDOW_SECONDS if exclude_user_id: rows = self.conn.execute( """ SELECT user_id, display_name, callback_url, accept_bottles, last_seen FROM users WHERE accept_bottles=1 AND last_seen>=? AND user_id<>? ORDER BY last_seen DESC """, (cutoff, exclude_user_id), ).fetchall() else: rows = self.conn.execute( """ SELECT user_id, display_name, callback_url, accept_bottles, last_seen FROM users WHERE accept_bottles=1 AND last_seen>=? ORDER BY last_seen DESC """, (cutoff,), ).fetchall() output = [] for row in rows: item = dict(row) item["last_seen_text"] = iso_time(int(item["last_seen"])) output.append(item) return output ``` ```python if parsed.path == "/api/users/online": qs = parse_qs(parsed.query) exclude = qs.get("exclude", [None])[0] users = self.db.online_users(exclude_user_id=exclude) json_response(self, 200, {"ok": True, "online_users": users}) return ``` ### Technical Analysis The heartbeat API accepts arbitrary callback URLs and stores them in SQLite. The online-user query selects those URLs, converts each complete row into a dictionary, and returns the result through an unauthenticated endpoint. The current implementation does not invoke callback URLs, so retaining and publishing them is not necessary for the implemented message-delivery workflow. Callback URLs may contain internal hostnames, unguessable webhook paths, q ...[truncated 1169 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/relay_server.py:661
Finding

Remote Relay Traffic Can Be Sent over Plaintext HTTP

Content
View full analysis
str: if public_base_url: return public_base_url.rstrip("/") if host in ("0.0.0.0", "::"): return f"http://127.0.0.1:{port}" return f"http://{host}:{port}" ``` ```python def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]: raw = json.dumps(payload, ensure_ascii=False).encode("utf-8") request = urllib.request.Request( url, data=raw, headers={"Content-Type": "application/json; charset=utf-8"}, method="POST", ) try: with urllib.request.urlopen(request, timeout=10) as response: return json.loads(response.read().decode("utf-8")) ``` ```python def get_json(url: str) -> Dict[str, Any]: request = urllib.request.Request(url, method="GET") try: with urllib.request.urlopen(request, timeout=10) as response: return json.loads(response.read().decode("utf-8")) ``` ### Technical Analysis The server generates `http://` base URLs unless an explicit public base URL is supplied, and the CLI accepts arbitrary relay URLs without enforcing TLS for non-loopback destinations. The transmitted payloads include stable user identifiers, display names, presence information, message text, replies, and bearer reply tokens. When a relay is accessed across a network using HTTP, those values are neither confidential nor protected against modification. The documented default of loopback HTTP is acceptable for strictly local use, but the implementation also supports cross-machine access and does not prevent unsafe remote plaintext operation. ### Attack Path 1. An operator exposes the relay on a network interface ...[truncated 884 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (16)

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The GET /api/inbox/{user_id} endpoint returns a user's received bottles, sent bottles, replies, and active reply URLs based solely on a user_id in the path, with no authentication or authorization checks. Any party who can guess or enumerate user IDs can read private message content and harvest reply tokens, enabling further unauthorized interaction and broad privacy compromise.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
91% confidence
Finding

This code file serves inbox contents for any supplied user_id via a GET route, returning received bottles, sent bottles, reply URLs, and replies. Although the module docstring describes a relay server, there is no confirmation, warning, or user-facing notice that requesting a user ID reveals that user's stored message history and reply tokens.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

该文档的说明、步骤和风险提示均仅以中文提供,没有给出用户可选择语言的机制,也没有说明这是面向特定中文地区或受众的限定性技能。根据规则,强制单一语言且无用户选择或正当化说明,属于自然语言层面的组织政策风险。

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

This markdown file presents all operational and safety information in a single language, Chinese, with no indication that users may choose another language or that the skill is intentionally limited to a Chinese-speaking audience. Under the stated policy, forcing a specific language without user opt-in is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
88% confidence
Finding

The skill metadata declares no explicit tool scope or permission boundaries even though the skill clearly relies on file access and network-facing behavior. In an agent ecosystem, missing scope declarations can cause overbroad execution privileges, make review harder, and increase the chance that a host grants more capability than the skill actually needs.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The skill description advertises reply links and browser-local identity storage but does not warn users that reply URLs may act as bearer tokens and that locally stored identity can be exposed to other local users, browser compromise, or accidental sharing. Because this skill is designed around messaging between users, the absence of privacy guidance increases the risk of impersonation, unauthorized replies, and disclosure of message relationships.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The document declares lang="zh-CN", and all visible UI text is Chinese-only, which imposes a specific language/locale on users. There is no indication in the file that language selection is optional or that the skill is intentionally limited to a China-specific context.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The code persistently stores user profile fields including userId, displayName, callbackUrl, and acceptance preference in localStorage. While this write is part of the UI flow, there is no visible warning, confirmation, or explanatory comment telling the user that their identity/profile data will be retained locally in the browser.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The file contains user-facing status and interface strings in Chinese, and the code does not offer any language selection or opt-in for locale. This can violate language/locale policy where skills must not force a specific language without user choice or clear region-specific justification.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The HTML declares zh-CN and all visible UI text is in Chinese, which imposes a specific language on users. Under the policy, forcing a locale without user opt-in or a documented region-specific justification is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The online_users flow selects and returns callback_url for active users, and /api/users/online exposes that list without access control. This leaks potentially sensitive internal or personal endpoints for all currently active users, which can aid targeting, SSRF pivot discovery, or user tracking.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The natural-language UI strings in the reply flow are hard-coded in Chinese, including success, error, and status messages. This imposes a specific language on users without any opt-in or locale selection, which matches the policy category for language or locale violations.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

This manifest contains operational instructions in Chinese, which imposes a specific language on readers without offering a language choice or documenting a justified locale restriction. Under the policy, language-specific instructions are a natural-language policy concern when no opt-in or region-specific rationale is provided.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

The entire demo session, including headings, instructions, and example UI interactions, is written in Chinese with no indication that another language is supported or that the locale is intentionally restricted for a region-specific use case. Under the stated policy, this is a natural-language locale constraint without user opt-in.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

The JSON includes a fixed Chinese message string, which reflects a language-specific output without any indication of user opt-in or locale selection. Under the policy rule for natural-language violations, hard-coding a specific language can be a locale policy issue when no choice or justification is provided.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
91% confidence
Finding

This markdown file contains user-facing operational instructions entirely in Chinese, including headings, expectations, and troubleshooting steps. Under the policy rule for natural-language violations, forcing a specific language without user opt-in can be a policy issue when no alternative language or justification is provided.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.