Back to skill

Security audit

Private Bridge

Security checks for vulnerabilities and agentic risk

Overview

This skill is a disclosed remote-control relay, but its implementation weakens the promised security around authentication and encrypted transport.

Review this carefully before installing. Use it only with a relay operator you trust, require a wss:// relay URL, avoid plaintext ws:// or http:// configuration, treat AUTH_TOKEN as a remote-control credential, and prefer a version that gates commands on successful authentication and restricts which workflows can be triggered remotely.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
relayClient.ts:64
Finding
Privileged Relay Commands Are Processed Before Authentication<![CDATA[ ## Vulnerability Details **File Location**: `relayClient.ts:64-75, 89-108` **Vulnerability Type**: Missing authentication-state enforcement **Risk Level**: High ### Vulnerable Code ```ts this.ws.onmessage = (event) => { try { const msg = JSON.parse(String(event.data)); this.handleMessage(msg); } catch { console.error("[private-bridge] Invalid message received"); } }; private handleMessage(msg: { type: string; data?: unknown }): void { switch (msg.type) { case "hello_ok": console.log("[private-bridge] Authenticated"); this.backoff = INITIAL_BACKOFF; this.connectionState = "online"; this.startHeartbeat(); break; case "prompt": case "status": case "restart": case "workflow": this.dispatch(msg as { type: string; data: IncomingMessage }); break; case "error": console.error("[private-bridge] Relay error:", msg.data); break; default: console.warn(`[private-bridge] Rejecting unknown type: ${msg.type}`); break; } } ``` ### Technical Analysis The client marks the connection as authenticated only after receiving `hello_ok`, but command dispatch does not verify that `connectionState` is `online` or that authentication has completed. Consequently, recognized command messages are passed to `dispatch()` immediately, including while the client is still in the `reconnecting` state and before the relay has accepted the authentication token. The affected commands invoke sensitive runtime capabilities: - `prompt` invokes `runtime.executePrompt()`. - `workflow` invokes `runtime.executeWorkflow()` with relay-controlled identifiers and parameters. - `status` discloses runtime health information. - `restart` invokes `runtime.restart()`. This contradicts the documented guarantee that relay commands are accepted only while the node is authenticated and online. Transport-level connection establishment is not equivalent to application-level authentication ...[truncated 1526 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add a dedicated authentication flag and initialize it to `false`. 2. Reset the flag whenever a connection starts, closes, errors, or is disconnected. 3. Set the flag to `true` only after a valid `hello_ok` response is received. 4. Reject all `prompt`, `status`, `restart`, and `workflow` messages unless both conditions hold: - The socket is open. - The client is authenticated and `connectionState === "online"`. 5. Consider closing the socket when a privileged command is received before authentication, because this indicates a protocol violation. 6. Validate the complete message schema before dispatch, including `request_id`, nested `data`, prompt type, workflow identifier, and parameter shape. 7. Apply explicit workflow allowlists and capability-level authorization rather than assuming every authenticated relay session may invoke every operation. 8. Add regression tests proving that all privileged message types are rejected before `hello_ok`, after closure, and during reconnection. Example hardening pattern: ```ts private authenticated = false; private openSocket(): void { this.authenticated = false; this.connectionState = "reconnecting"; // Open the socket... } private handleMessage(msg: { type: string; data?: unknown }): void { if (msg.type === "hello_ok") { this.authenticated = true; this.connectionState = "online"; this.startHeartbeat(); return; } const privilegedTypes = new Set([ "prompt", "status", "restart", "workflow", ]); if (privilegedTypes.has(msg.type)) { if (!this.authenticated || this.connectionState !== "online") { console.warn("[private-bridge] Rejecting command before authentication"); this.ws?.close(1008, "authentication required"); return; } this.dispatch(msg as { type: string; data: IncomingMessage }); } } ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
config.ts:7
Finding
Plaintext WebSocket Configuration Exposes Authentication Tokens and Remote-Control Traffic<![CDATA[ ## Vulnerability Details **File Location**: `config.ts:7-21` **Vulnerability Type**: Cleartext transmission of sensitive information **Risk Level**: High ### Vulnerable Code ```ts export function validateConfig(config: Partial<RemoteRelayConfig>): RemoteRelayConfig { if (!config.relay_url) throw new Error("private-bridge: relay_url is required"); if (!config.node_id) throw new Error("private-bridge: node_id is required"); if (!config.auth_token) throw new Error("private-bridge: auth_token is required"); // Normalize URL: ensure wss:// for secure connections let url = config.relay_url.replace(/\/+$/, ""); url = url.replace(/^https:\/\//, "wss://"); url = url.replace(/^http:\/\//, "ws://"); return { relay_url: url, node_id: config.node_id, auth_token: config.auth_token, }; } ``` The insecure behavior is explicitly expected by `config.test.ts:18-20`: ```ts it("converts http to ws", () => { const result = validateConfig({ ...valid, relay_url: "http://localhost:8080" }); expect(result.relay_url).toBe("ws://localhost:8080"); }); ``` The token is subsequently sent over the configured connection in `relayClient.ts:52-61`: ```ts this.ws.onopen = () => { console.log("[private-bridge] Connected, sending auth"); this.send({ type: "hello", data: { node_id: this.config.node_id, token: this.config.auth_token, meta: { kind: "openclaw" }, }, }); }; ``` ### Technical Analysis Configuration validation deliberately converts `http://` relay URLs to `ws://`, permitting an unencrypted WebSocket connection. The initial application handshake sends the authentication token and node identifier over this channel. Subsequent traffic can include remote prompts, model response tokens, workflow parameters and results, health information, error details, and process-restart commands. A plaintext WebSocket provides neither confidentiality nor transport integrity. An attacker with a suitable network position ...[truncated 2168 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse the relay URL with the standard `URL` API instead of modifying it through regular-expression replacements. 2. Accept only the `wss:` scheme in production. 3. Convert `https:` to `wss:` only if backward-compatible input normalization is required. 4. Reject `ws:`, `http:`, unknown schemes, malformed URLs, and URLs containing embedded usernames or passwords. 5. If plaintext localhost connections are necessary for development, require a separate explicit development-only option and restrict the hostname to loopback addresses. 6. Update tests so `http://` and `ws://` inputs are rejected rather than accepted. 7. Align the implementation with the documented TLS-only security guarantee. 8. Rotate any authentication token that may previously have traversed a plaintext connection. 9. Consider application-layer message authentication or signed commands as defense in depth, while retaining TLS as mandatory transport protection. Example validation approach: ```ts export function validateConfig( config: Partial<RemoteRelayConfig> ): RemoteRelayConfig { if (!config.relay_url) { throw new Error("private-bridge: relay_url is required"); } if (!config.node_id) { throw new Error("private-bridge: node_id is required"); } if (!config.auth_token) { throw new Error("private-bridge: auth_token is required"); } const normalized = config.relay_url.startsWith("https://") ? config.relay_url.replace(/^https:/, "wss:") : config.relay_url; const relayUrl = new URL(normalized); if (relayUrl.protocol !== "wss:") { throw new Error("private-bridge: relay_url must use wss://"); } if (relayUrl.username || relayUrl.password) { throw new Error("private-bridge: embedded URL credentials are not allowed"); } relayUrl.pathname = relayUrl.pathname.replace(/\/+$/, ""); return { relay_url: relayUrl.toString().replace(/\/$/, ""), node_id: config.node_id, auth_token: config.auth_token, }; ...[truncated 9 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (5)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The declared description presents the skill as a secure outbound-only relay for remote OpenClaw control, but the supplied code shows concrete remote-control capabilities beyond a generic relay transport description. Specifically, it supports inbound command types for prompt execution, workflow execution, status queries, and restart actions, and requires the host runtime to implement executePrompt, executeWorkflow, and restart. It also defines outbound telemetry/status structures including uptime, active task count, last error, and connection state. While this is broadly related to remote control, the description omits these sensitive operational capabilities, especially remote prompt execution and restart control. The snippet does not mention SSH or Telegram, so those parts are neither confirmed nor contradicted here, but the undeclared command-and-control functionality is significant enough to constitute a mismatch.

Ae1

High
Category
analysis-evasion
Content
files: ["relayClient.ts", "config.ts", "capabilities.ts", "index.ts"]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code opens a WebSocket connection and sends identifying information and an auth token in the hello message. It also later transmits prompt output and heartbeat/status data, but the file only includes developer logs and brief method comments, not any user-facing disclosure or warning about network transmission of potentially sensitive data.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The client accepts authenticated remote messages that can directly trigger restart and arbitrary workflow execution via runtime.restart() and runtime.executeWorkflow() with no local authorization, confirmation, allowlist, or capability checks visible in this file. In the context of a remote-control relay for OpenClaw, this meaningfully expands the attack surface: compromise of the relay, token theft, or misrouting of messages could result in remote service disruption or execution of sensitive automated actions.

Intent-Code Divergence

Low
Confidence
98% confidence
Finding
The manifest identifies this skill as "remote-relay", but the init() docstring says it initializes the "PrivateBridge skill." This is an active documentation mismatch that can mislead integrators about what component they are invoking, even though the code itself constructs a RelayClient.

Static analysis

No suspicious patterns detected.