fix(remote-server): cap WS maxPayload (256 KiB) + guard sendToClient — close a pre-auth parse freeze-sink and a send-throw crash

Two isolated hardenings of the opt-in remote/diagnostics WS server, surfaced by
the session-wide diagnostics audit:

1. WebSocketServer was created with no maxPayload, so ws defaults to 100 MiB per
   message. The connection handler runs JSON.parse(raw) on the FIRST message
   (the auth frame) before authentication, so any peer past the IP allowlist
   could send a huge payload and force a synchronous multi-MB JSON.parse on the
   main-process event loop — an unbounded freeze/DoS sink. Diag, auth and WebRTC
   signaling messages are all small; cap maxPayload at 256 KiB to close it.

2. sendToClient did ws.send(JSON.stringify(data)) with no readyState/try guard
   (unlike broadcast, which checks ws.readyState === 1). A send on a closing
   socket, or a stringify throw, escaped the diag-response callback as an
   uncaughtException — a potential crash. Mirror broadcast: send only when
   readyState === 1, wrapped in try/catch.

Both are isolated to the transport layer with zero redaction surface. The audit's
larger finding — server_health doing O(historySize) synchronous work per request
(6-7 full-config clones + unbounded history walks) — is a real freeze, but ONLY on
the cold opt-in diagnostics path with a large history (this user: 23 rows), and the
safe fix cuts into the credential-redaction collectors (which have leaked twice);
deferred and documented in tasks/todo.md rather than operated under risk.

397/397 tests pass, eslint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-06-21 03:06:08 +02:00
parent 6dcc98f52d
commit 0809c75d50

View File

@ -21,7 +21,7 @@ class RemoteServer {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this._config = opts; this._config = opts;
const wssOpts = { port: opts.port }; const wssOpts = { port: opts.port, maxPayload: 256 * 1024 };
if (opts.host) wssOpts.host = opts.host; if (opts.host) wssOpts.host = opts.host;
this._wss = new WebSocketServer(wssOpts, () => { this._wss = new WebSocketServer(wssOpts, () => {
resolve(); resolve();
@ -175,7 +175,9 @@ class RemoteServer {
sendToClient(clientId, data) { sendToClient(clientId, data) {
for (const [ws, client] of this._clients) { for (const [ws, client] of this._clients) {
if (client.id === clientId && client.authenticated) { if (client.id === clientId && client.authenticated) {
ws.send(JSON.stringify(data)); if (ws.readyState === 1) {
try { ws.send(JSON.stringify(data)); } catch {}
}
break; break;
} }
} }