Back to skill

Security audit

OpenClaw Agent Mesh

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate peer-messaging purpose, but its network-facing inbox handling has serious validation gaps that could let reachable peers write JSON files outside the intended state directory.

Review before installing or running as a server. Use it only on a trusted network, bind to 127.0.0.1 unless remote peers are required, keep the state directory under a low-privilege account, and fix identifier validation/path containment before accepting network traffic.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/mesh.py:149
Finding
Network-Controlled Path Traversal Allows Arbitrary JSON File Writes<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mesh.py:149-150, 163-175, 185-199, 243-253` **Vulnerability Type**: Path traversal and unsafe file creation **Risk Level**: High ### Vulnerable Code ```python def peer_path(agent_id: str) -> pathlib.Path: return PEERS_DIR / f"{agent_id}.json" ``` ```python def cmd_receive_contact(args): payload = load_json(pathlib.Path(args.file)) unsigned = {k: v for k, v in payload.items() if k != "signature"} sender = payload.get("from", {}) ok = verify_signature(unsigned, payload.get("signature", ""), sender.get("public_key", "")) result = {"verified": ok, "request_id": payload.get("request_id")} if ok: path = REQ_IN_DIR / f"{payload['request_id']}.json" save_json(path, payload) result["saved"] = str(path) print(json.dumps(result, indent=2)) ``` ```python def cmd_approve_request(args): p = REQ_IN_DIR / f"{args.request_id}.json" if not p.exists(): raise SystemExit("Request not found") payload = load_json(p) sender = payload["from"] peer = { "agent_id": sender["agent_id"], "display_name": sender.get("display_name"), "endpoint": sender.get("endpoint"), "public_key": sender["public_key"], "fingerprint": sender["fingerprint"], "trusted_at": now_iso(), "source_request_id": payload["request_id"], } save_json(peer_path(sender["agent_id"]), peer) ``` ```python def cmd_receive_message(args): payload = load_json(pathlib.Path(args.file)) sender_id = payload.get("from") peer = load_peer(sender_id) unsigned = {k: v for k, v in payload.items() if k != "signature"} ok = verify_signature(unsigned, payload.get("signature", ""), peer["public_key"]) result = {"verified": ok, "message_id": payload.get("message_id")} if ok: path = MSG_IN_DIR / f"{payload['message_id']}.json" save_json(path, payload) result["saved"] = str(path) ``` # ...[truncated 2161 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Validate identifiers before any filesystem operation. For example: - `request_id`: `^req_[A-Fa-f0-9]{32}$` - `message_id`: `^msg_[A-Fa-f0-9]{32}$` - `agent_id`: a conservative allowlist of letters, digits, underscores, and hyphens with a fixed maximum length. - Resolve each destination path and verify that it remains below its intended root using `Path.resolve()` and `Path.relative_to()`. - Apply containment checks to reads, writes, and deletions, including approval and rejection commands. - Create new inbound records exclusively rather than silently overwriting existing files. - Validate that the supplied fingerprint matches the supplied public key before storing a contact request. - Keep the service account restricted to its dedicated state directory wherever operationally possible. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/mesh.py:163
Finding
Missing Envelope Freshness, Recipient, Schema, and Replay Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mesh.py:163-175, 243-255` **Vulnerability Type**: Insufficient authentication-envelope validation and replay protection **Risk Level**: Medium ### Vulnerable Code ```python def cmd_receive_contact(args): payload = load_json(pathlib.Path(args.file)) unsigned = {k: v for k, v in payload.items() if k != "signature"} sender = payload.get("from", {}) ok = verify_signature(unsigned, payload.get("signature", ""), sender.get("public_key", "")) result = {"verified": ok, "request_id": payload.get("request_id")} if ok: path = REQ_IN_DIR / f"{payload['request_id']}.json" save_json(path, payload) result["saved"] = str(path) print(json.dumps(result, indent=2)) ``` ```python def cmd_receive_message(args): payload = load_json(pathlib.Path(args.file)) sender_id = payload.get("from") peer = load_peer(sender_id) unsigned = {k: v for k, v in payload.items() if k != "signature"} ok = verify_signature(unsigned, payload.get("signature", ""), peer["public_key"]) result = {"verified": ok, "message_id": payload.get("message_id")} if ok: path = MSG_IN_DIR / f"{payload['message_id']}.json" save_json(path, payload) result["saved"] = str(path) result["ack"] = {"type": "ack", "message_id": payload["message_id"], "status": "received", "timestamp": now_iso()} print(json.dumps(result, indent=2)) ``` ### Technical Analysis Signature validity is treated as sufficient for acceptance. The receiver does not verify: - That `type` is the expected envelope type. - That all mandatory fields are present and have the correct type. - That `timestamp` is parseable, recent, and not excessively far in the future. - That a direct message's `to` field identifies the local agent. - That a request or message ID has not already been processed. - That the contact-request fingerprint corresponds to its public key. These omissions cont ...[truncated 1293 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Define and enforce strict schemas for contact requests and direct messages before using any field. - Require the exact expected `type` and reject missing, unknown, or incorrectly typed fields. - Parse timestamps as timezone-aware UTC values and enforce configurable maximum age and future-clock-skew limits. - Require a direct message's `to` value to match the local identity's `agent_id`. - Reject identifiers that already exist instead of overwriting their files. - Maintain a durable replay cache if records may be archived or deleted. - Recompute and compare the contact request's public-key fingerprint. - Return a successful acknowledgement only after every validation and persistence step succeeds. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/server.py:36
Finding
Unbounded HTTP Request Bodies Permit Denial of Service<![CDATA[ ## Vulnerability Details **File Location**: `scripts/server.py:36-39, 84-85` **Vulnerability Type**: Uncontrolled resource consumption **Risk Level**: Medium ### Vulnerable Code ```python def do_POST(self): length = int(self.headers.get('Content-Length', '0')) raw = self.rfile.read(length) try: payload = json.loads(raw.decode('utf-8') or '{}') ``` ```python httpd = HTTPServer((args.host, args.port), Handler) print(json.dumps({'ok': True, 'host': args.host, 'port': args.port, 'state_dir': args.state_dir})) httpd.serve_forever() ``` ### Technical Analysis The server converts `Content-Length` directly to an integer and reads the requested amount without imposing a protocol-level maximum. The entire body is then decoded and parsed as JSON in memory. The use of the single-threaded `HTTPServer` compounds this issue: one client delivering a large body or transmitting it slowly can occupy the only request handler and prevent discovery, contact, and message operations from being served. ### Attack Path 1. The server is started on its default `0.0.0.0` host and becomes reachable by another network client. 2. An unauthenticated attacker opens a connection to either POST endpoint. 3. The attacker declares an excessively large `Content-Length` and sends a large payload, causing memory and CPU consumption during reading and JSON parsing. 4. Alternatively, the attacker sends the declared body very slowly. 5. Because the server handles one request at a time and has no explicit body or socket timeout controls, legitimate mesh operations are delayed or denied. ### Impact Assessment Any client that can reach the server can degrade or exhaust service availability without first becoming a trusted peer. The likely impact includes excessive memory use, blocked request processing, and denial of peer discovery or message receipt. This flaw does not itself grant filesystem or code-execution privileges. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Reject absent, malformed, negative, or excessive `Content-Length` values before reading the body. - Set a small protocol-appropriate maximum body size, such as tens or hundreds of kilobytes. - Apply socket read and request timeouts to mitigate slow-client attacks. - Consider a bounded concurrent server implementation so one connection cannot block all clients. - Add reverse-proxy or firewall rate limits when exposing the endpoint beyond a trusted LAN. - Validate `Content-Type` before processing and reject unsupported transfer modes. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/mesh.py:27
Finding
Mesh Messages and State Files May Have Excessively Permissive Local Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/mesh.py:27-29, 42-44, 139-142` **Vulnerability Type**: Insecure permissions for sensitive local data **Risk Level**: Low ### Vulnerable Code ```python def ensure_dirs() -> None: for p in [STATE_DIR, PEERS_DIR, REQ_IN_DIR, REQ_OUT_DIR, MSG_IN_DIR, MSG_OUT_DIR]: p.mkdir(parents=True, exist_ok=True) ``` ```python def save_json(path: pathlib.Path, obj: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(obj, indent=2, ensure_ascii=False) + "\n") ``` ```python PRIVATE_KEY_PATH.write_bytes(private_pem) PUBLIC_KEY_PATH.write_bytes(public_pem) os.chmod(PRIVATE_KEY_PATH, 0o600) save_json(IDENTITY_PATH, identity) ``` ### Technical Analysis The private key is explicitly restricted to mode `0600`, but the state directories and JSON files rely on the process umask. Under common umask settings, directories may be created as `0755` and files as `0644`. These files contain plaintext direct messages, contact-request purposes, endpoint information, public identity metadata, and trust relationships. On a multi-user system, relying on ambient umask configuration may allow other local users to traverse the state directory and read this information. ### Attack Path 1. A user initializes and runs the Skill under a default or permissive umask. 2. Mesh directories and JSON records are created with group- or world-readable permissions. 3. Another local account enumerates `~/.openclaw/agent-mesh`. 4. That account reads stored incoming and outgoing messages, requests, or peer metadata without authorization. ### Impact Assessment Other local users may obtain plaintext communications and mesh relationship metadata. The private signing key is explicitly protected after initialization and is not directly exposed by this finding. Scope is limited to local accounts that can traverse the relevant parent directories. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Create the state root and all sensitive subdirectories with mode `0700`. - Create identity, peer, request, and message files with mode `0600`. - Use secure file creation primitives that set restrictive permissions at creation time rather than correcting them afterward. - Audit existing state at startup and reject or repair unsafe permissions. - Document that the state directory contains plaintext communication records. - Preserve the existing `0600` protection for the private key and consider checking its owner and mode before every signing operation. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • 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
Findings (6)

Tp4

High
Category
MCP Tool Poisoning
Confidence
91% confidence
Finding
Most of the declared workflow is broadly represented: identity init, discovery response generation, contact request creation/receipt, approval/rejection, trust storage, and direct message send/receive with signatures. However, key parts of the description overstate the implemented behavior. The code is purely a CLI utility and local file store; it does not start or run any HTTP server despite the description explicitly claiming lightweight HTTP server support for discovery and inbox handling. Its 'scan' command is not LAN scanning or nearby-node discovery in the usual sense—it only fetches /agent-mesh/discovery from URLs explicitly passed as arguments. Also, while approval/rejection exists locally, there is no code to transmit approvals or rejections back to requesting peers. These are material behavior gaps relative to the declared description, so this should be flagged as a mismatch.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run_mesh(args, state_dir: str):
    env = os.environ.copy()
    env['OPENCLAW_AGENT_MESH_DIR'] = state_dir
    res = subprocess.run([sys.executable, str(MESH)] + args, env=env, capture_output=True, text=True)
    return res.returncode, res.stdout, res.stderr
Confidence
60% confidence
Finding
Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill describes capabilities that require filesystem access, shell execution, networking, and likely environment access, but it does not declare any explicit tool scope or permission boundaries. In an agent framework, missing scope declarations can cause over-broad execution privileges, making it easier for the skill to read/write unintended files, invoke arbitrary commands, or perform network actions without clear user consent.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The documentation instructs users to run a server bound to 0.0.0.0, which exposes it on all network interfaces, but it does not warn about the security implications. In the context of a peer-messaging service that accepts discovery, contact requests, and messages, this can unintentionally expose an inbox endpoint to untrusted local networks or broader networks if the host is misconfigured.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run(cmd: List[str], input_bytes: bytes | None = None) -> subprocess.CompletedProcess:
    return subprocess.run(cmd, input=input_bytes, capture_output=True, check=True)


def generate_keypair() -> tuple[bytes, bytes]:
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_mesh(args, state_dir: str):
    env = os.environ.copy()
    env['OPENCLAW_AGENT_MESH_DIR'] = state_dir
    res = subprocess.run([sys.executable, str(MESH)] + args, env=env, capture_output=True, text=True)
    return res.returncode, res.stdout, res.stderr
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Static analysis

No suspicious patterns detected.