Back to skill

Security audit

openclaw-bottle-drift

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent messaging skill, but its relay can expose private messages and allow identity spoofing when reachable by other clients because it has no real authentication.

Install only for local testing or a trusted, loopback-only environment. Do not expose the relay on a shared network or the public internet until it has real authentication, authorization, removal of reply tokens from sender/general inbox responses, and filtering of callback URLs from public directory responses.

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:411
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() ``` ```python if parsed.path.startswith("/api/inbox/"): user_id = validate_user_id(parsed.path.rsplit("/", 1)[-1]) json_response(self, 200, self.db.inbox(user_id)) return ``` ### Technical Analysis The inbox endpoint treats a caller-provided `user_id` as sufficient proof of identity. It does not require an authenticated session, API credential, signed request, or ownership check. The response contains received messages, sent messages, replies, recipient identities, and `reply_token` values. These tokens are a ...[truncated 1244 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/relay_server.py:227
Finding

Caller-Controlled User IDs Permit Identity Spoofing and Profile Overwrites

Content
View full analysis
Dict[str, Any]: ts = now_ts() self.conn.execute( """ INSERT INTO users(user_id, display_name, callback_url, accept_bottles, last_seen) VALUES (?, ?, ?, ?, ?) ON CONFLICT(user_id) DO UPDATE SET display_name=excluded.display_name, callback_url=excluded.callback_url, accept_bottles=excluded.accept_bottles, last_seen=excluded.last_seen """, (user_id, display_name, callback_url, 1 if accept_bottles else 0, ts), ) ``` ```python if parsed.path == "/api/presence/heartbeat": data = self._parse_json() user_id = validate_user_id(str(data.get("user_id", ""))) display_name = validate_name(str(data.get("display_name", "")), "display_name") callback_url = str(data.get("callback_url", "")).strip() or None accept_bottles = bool(data.get("accept_bottles", True)) result = self.db.heartbeat(user_id, display_name, callback_url, accept_bottles) json_response(self, 200, result) return if parsed.path == "/api/bottles/send": data = self._parse_json() sender_id = validate_user_id(str(data.get("sender_id", "")), "sender_id") sender_name = validate_name(str(data.get("sender_name", "")), "sender_name") message = validate_message(str(data.get("message", "")), "message") fanout = int(data.get("fanout", 1)) ttl_seconds = int(data.get("ttl_seconds", DEFAULT_TTL)) result = self.db.create_bottle( sender_id=sender_id, sender_name=sender_name, message=message, fanout=fanout, ttl_seconds=ttl_seconds, ) ``` ### Techn ...[truncated 1440 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/relay_server.py:315
Finding

Recipient Reply Capability Is Disclosed to the Bottle Sender

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/relay_server.py:257
Finding

Unauthenticated Online Directory 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 online-user endpoint is unauthenticated and serializes complete selected database rows. Those rows include `callback_url`, even though the dashboard only needs the user ID, display name, and presence timestamp. Callback URLs can contain unguessable webhook paths, credentials embedded in query parameters, private hostnames, internal addresses, or other deployment metadata. Returning them violates data minimization and exceeds the minimum privileges needed to display online subscribers. A ...[truncated 1060 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (17)

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The send API returns each recipient's reply_token and reply_url to the sender immediately after delivery selection. Because the reply token is the sole credential for submitting a reply, this lets the sender impersonate recipients and forge replies to their own bottle, defeating the trust model of recipient-only responses.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The GET /api/inbox/{user_id} endpoint exposes a user's received bottles, sent bottles, replies, and embedded reply URLs/tokens based solely on a user-supplied identifier, with no authentication or authorization check. Any party who can guess or enumerate user IDs can read private messages and harvest reply tokens to submit unauthorized replies, causing both confidentiality and integrity loss.

Content

No source excerpt is available for this finding.

Missing User Warnings

High
Category
Not specified by scanner
Confidence
95% confidence
Finding

This finding describes the same underlying issue as the unauthenticated inbox endpoint: sensitive stored messages and reply tokens are exposed with no access control. The absence of any warning is secondary; the core security problem is direct unauthorized disclosure of private content and reply capabilities.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

该 README 全文以中文撰写,未见提供多语言选项、用户可选择语言的说明,或对必须使用中文的区域/合规性理由。根据规则,强制特定语言而无用户选择属于自然语言层面的组织政策风险。

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 self-check content in a single language, which can violate language/locale policy when no user opt-in or alternative language path is provided. The file contains no indication that the skill is region-specific or that Chinese is an intentional, documented constraint.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding

The skill declares platform/runtime requirements and references scripts/resources, but it does not declare any explicit tool scope or permissions despite requiring network and file access to operate. This weakens least-privilege controls and makes it harder for a host platform or reviewer to understand and constrain what the skill is allowed to do, increasing the blast radius if the implementation is unsafe or compromised.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill promotes dedicated reply links that can be shared into OpenClaw sessions or external webpages, but it does not warn that possession of the link may grant the ability to submit a reply. Because these links function like bearer tokens, accidental forwarding, logging, browser history exposure, or public sharing could let unauthorized parties read or send replies.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The documentation states that identity is stored locally in the browser and lacks its own account system, but it does not warn users that local browser state can be modified, copied, or lost. In this context, users may assume the displayed identity is authenticated when it is only client-side state, which can enable impersonation, spoofed messages, or privacy misunderstandings.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The HTML document declares lang="zh-CN", and all visible UI text is presented in Chinese, which indicates the skill is fixed to a specific language/locale. The file does not provide any user opt-in, language switch, or documented region-specific justification, matching the policy-violation criteria for forced language or locale.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

This code performs multiple HTTP requests that send user and system data to a relay service, including presence data, bottle messages, replies, and callback URLs. While the parser help names the commands, it does not clearly warn users that these fields are transmitted over the network or that callback URLs may disclose endpoint information.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The server returns reply_token and derived reply_url values in API responses, and those tokens grant the ability to submit a reply for a delivery. The code does not include any user-facing disclosure, comment, or docstring warning that these values are bearer-style secrets and should be handled carefully.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

Several user-visible status and error messages in the reply flow are hardcoded in Chinese, such as the already-replied notice and success/failure text. The file does not provide any locale selection, opt-in, or documented justification for requiring Chinese, which conflicts with the language-choice policy.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The online users API returns callback_url for all active users without access control. This leaks internal or private endpoint information, enabling user enumeration, infrastructure mapping, and potential follow-on attacks against connected nodes.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

Lines L25-L28 use Chinese-language instructions in a manifest that otherwise uses English, which can impose a language constraint on operators without opt-in. The file does not state that the skill is intended only for Chinese-speaking users or provide an alternative language.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
93% confidence
Finding

This JavaScript file contains user-facing status text in Chinese, and the same pattern continues throughout the UI strings. Because the skill presents a fixed language without any visible opt-in or locale selection, it may violate the language/locale policy requirement.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The HTML declares lang="zh-CN" and all visible UI text is written in Simplified Chinese, which effectively constrains the skill interaction to a specific language. The file does not indicate that this is an optional locale, user-selected preference, or a region-specific requirement.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
89% confidence
Finding

This markdown file contains user-facing operational instructions exclusively in Chinese, including headings, steps, and expected results. Under the policy rule for natural-language constraints, forcing a specific language without opt-in can be a locale/language policy violation when no alternative or justification is provided.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.