T09 · Insecure Skill Coding Practices
- Location
client.py:61- Finding
Unauthenticated WebSocket Messages Are Delegated to the Main Agent
- Content
View full analysis
Vulnerability Details
File Location:
client.py:61-68
Related Documentation:skill.md:56-70,skill.md:74-80
Vulnerability Type: Untrusted input delegated across a privileged agent trust boundary
Risk Level: MediumVulnerable Code
python async def cmd_agent(url: str, timeout: float) -> None: """Listen for user messages, forward each to openclaw agent, send the response back.""" async with aiohttp.ClientSession() as session: async with session.ws_connect(url) as ws: try: async with asyncio.timeout(timeout): async for msg in ws: if msg.type == aiohttp.WSMsgType.TEXT: data = json.loads(msg.data) if data.get("type") != "message": continue user_text = data.get("content", "") print(f"[user] {user_text}", flush=True) reply = await run_agent(user_text) print(f"[agent] {reply}", flush=True) await ws.send_json({"type": "message", "content": reply})The target agent is selected here:
python async def run_agent(message: str) -> str: """Run openclaw agent and return its stdout.""" cmd = f"openclaw agent --agent main --message {shlex.quote(message)}" proc = await asyncio.create_subprocess_shell( cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE )The endpoint is configurable and defaults to plaintext WebSocket transport:
python DEFAULT_URL = "ws://localhost:3111/connect"python parser.add_argument("--url", default=DEFAULT_URL, help=f"WebSocket URL (default: {DEFAULT_URL})")Technical Analysis
The agent bridge accepts JSON messages from a WebSocket connection and forwards every message whose
typeequalsmessagedirectly toopenclaw agent --agent main...[truncated 2777 chars]- Remediation
View remediation
Remediation Suggestions
-
Use a dedicated least-privileged agent
- Replace
--agent mainwith a purpose-built voice agent. - Remove unnecessary tools, credentials, filesystem access, and sensitive context.
- Enforce an explicit allowlist of operations appropriate for voice requests.
- Replace
-
Authenticate and authorize the WebSocket peer
- Require a cryptographically strong session token or mutually authenticated TLS.
- Validate the peer before accepting any message.
- Bind authorization to the expected user and session rather than treating every connected server as trusted.
-
Require protected transport
- Require
wss://for every non-loopback endpoint. - Reject plaintext remote WebSocket URLs.
- Validate certificates and restrict endpoints to an explicit hostname or URL allowlist.
- Require
-
Apply an agent-input policy boundary
- Validate that
contentis a string and enforce strict size limits. - Reject unsupported message structures and excessive nesting.
- Introduce task-specific policy checks before delegating input.
- Do not rely on shell quoting to defend against prompt-level attacks.
- Validate that
-
Restrict outbound responses
- Do not return unrestricted agent output automatically.
- Detect or redact credentials, secrets, internal paths, and sensitive contextual data.
- Enforce response-size limits and return structured, task-specific results where possible.
-
Harden subprocess execution
- Avoid invoking a shell even though
shlex.quotecurrently mitigates ordinary POSIX shell injection. - Use an argument-vector subprocess call such as:
- Avoid invoking a shell even though
python proc = await asyncio.create_subprocess_exec( "openclaw", "agent", "--agent", "voice-limited", "--message", message, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, )- Add operational safeguards
- Rate-limit messages and bound agent execution time.
- Limit concurrent requests.
- Rec ...[truncated 183 chars]
-
