T05 · Unauthorized Access and Privilege Escalation
Error
- Location
- bin/serve.js:190
- Finding
- Unauthenticated WebSocket Proxy Grants Privileged Gateway Access<![CDATA[ ## Vulnerability Details **File Location**: `bin/serve.js:190-226`, `bin/serve.js:302-323`, `bin/serve.js:337-358`, `bin/serve.js:563-573` **Vulnerability Type**: Missing client authentication and authorization on a privileged WebSocket proxy **Risk Level**: High ### Vulnerable Code ```js function buildConnectFrame(nonce) { const signedAtMs = Date.now(); const scopes = [ "operator.admin", "operator.read", "operator.write", "operator.approvals", "operator.pairing", ]; const payload = buildV3Payload({ deviceId, clientId: "cli", clientMode: "cli", role: "operator", scopes, signedAtMs, token: gatewayToken, nonce, platform: "node", }); const signature = signPayload(privateKeyPem, payload); return JSON.stringify({ type: "req", id: `${Date.now().toString(16)}-proxy`, method: "connect", params: { minProtocol: 3, maxProtocol: 3, client: { id: "cli", displayName: "Clawface Web", version: "1.0", platform: "node", mode: "cli", instanceId: `clawface-${Date.now()}`, }, locale: "en-US", userAgent: "clawface-web", role: "operator", scopes, caps: [], device: { id: deviceId, publicKey: publicKeyBase64Url, signature, signedAt: signedAtMs, nonce, }, auth: { token: gatewayToken }, }, }); } ``` ```js // Browser → gateway browserSocket.on("data", (chunk) => { brBuf = Buffer.concat([brBuf, chunk]); while (true) { const frame = decodeFrame(brBuf); if (!frame) break; brBuf = brBuf.subarray(frame.totalLen); if (frame.opcode === 0x08) { sendClose(gwSocket, true); gwSocket.end(); browserSocket.end(); return; } if (frame.opcode === 0x09) { sendPong(browserSocket, frame.payload, false); continue; } if (frame.opcode !== 0x01) continue; // Forward browser frames to gateway (must be masked per RFC 6455) sendText(gwSocket, frame.payload.toString("utf8"), true); ...[truncated 3432 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Bind the HTTP service explicitly to a loopback address: ```js server.listen(port, "127.0.0.1", () => { console.log(`http://127.0.0.1:${port}`); }); ``` 2. Generate a cryptographically random, per-launch session token and require it during the WebSocket handshake. 3. Validate `Origin` against an explicit allowlist and reject missing or unexpected origins. 4. Validate `Host` and reject requests addressed through unapproved hostnames. 5. Do not provide a transparent arbitrary-message proxy. Parse browser messages and allowlist only the gateway methods needed by the avatar interface. 6. Request only the minimum gateway scopes necessary for chat functionality. Remove administrative, approval, and pairing scopes unless a documented feature strictly requires them. 7. Enforce message-size, connection-count, rate, and idle-time limits. 8. Return an error and close the connection if gateway credentials are incomplete or upstream authentication fails. 9. Consider using a narrowly scoped server-side API instead of exposing the gateway protocol directly to browser clients. ]]>
