From 46310682741b9c2f439d86cb8253fe8fccebb309 Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Tue, 21 Jul 2026 14:42:47 +1200 Subject: [PATCH 01/17] Validate WebSocket binary payloads --- src/services/webSocketClient.ts | 19 +++++++++++++------ test/webSocketSupport.test.ts | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/services/webSocketClient.ts b/src/services/webSocketClient.ts index 055f6c8..6735014 100644 --- a/src/services/webSocketClient.ts +++ b/src/services/webSocketClient.ts @@ -376,6 +376,7 @@ export class WebSocketClient implements vscode.Disposable { onProgress?.('Resolving WebSocket request...'); const resolved = await this.buildResolvedRequest(request, collection, folderDefaults, extraVariables, environmentName); + const payload = resolved.message ? this._messagePayload(resolved.message) : undefined; const timeoutMs = vscode.workspace.getConfiguration('missio').get('timeout', 30000); onProgress?.('Connecting WebSocket...'); @@ -417,14 +418,13 @@ export class WebSocketClient implements vscode.Disposable { socket.on('open', () => { event({ direction: 'event', type: 'open' }); - if (!resolved.message) { + if (!resolved.message || payload === undefined) { onProgress?.('Disconnecting WebSocket...'); socket.close(1000, 'Missio disconnect'); return; } onProgress?.('Sending WebSocket message...'); - const payload = this._messagePayload(resolved.message); socket.send(payload); event({ direction: 'outbound', @@ -620,10 +620,17 @@ export class WebSocketClient implements vscode.Disposable { private _messagePayload(message: WebSocketMessage): string | Buffer { if (message.type !== 'binary') return message.data; - const normalized = message.data.trim(); - return /^[A-Za-z0-9+/=\r\n]+$/.test(normalized) - ? Buffer.from(normalized, 'base64') - : Buffer.from(message.data, 'utf-8'); + const normalized = message.data.replace(/\s/g, ''); + const isBase64 = normalized.length > 0 + && normalized.length % 4 === 0 + && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(normalized); + if (!isBase64) { + throw Object.assign( + new Error('Binary WebSocket message data must be valid base64.'), + { code: 'MISSIO_INVALID_WEBSOCKET_BINARY_DATA' }, + ); + } + return Buffer.from(normalized, 'base64'); } private _buildResponse( diff --git a/test/webSocketSupport.test.ts b/test/webSocketSupport.test.ts index 7182ad9..b443c58 100644 --- a/test/webSocketSupport.test.ts +++ b/test/webSocketSupport.test.ts @@ -359,6 +359,22 @@ describe('WebSocket execution lifecycle', () => { }); }); + it('rejects malformed binary payloads instead of decoding them lossily', async () => { + const client = new WebSocketClient(makeEnvService({ tenant: 'nz' })); + + await expect(client.send({ + info: { name: 'Invalid binary', type: 'websocket' }, + websocket: { + url: 'ws://127.0.0.1:1/ws/echo', + message: { type: 'binary', data: 'ABC' }, + }, + }, makeCollection())).rejects.toMatchObject({ + code: 'MISSIO_INVALID_WEBSOCKET_BINARY_DATA', + message: 'Binary WebSocket message data must be valid base64.', + }); + expect(client.activeConnectionCount).toBe(0); + }); + it('handles server close and explicit cancellation without leaking active sockets', async () => { const fixture = await startFixture(); const envService = makeEnvService({ wsBaseUrl: fixture.baseUrl, tenant: 'nz' }); From a2f2b2e00a4acab85058a9eedb41d50f7e509223 Mon Sep 17 00:00:00 2001 From: Chris Johnstone Date: Tue, 21 Jul 2026 15:31:33 +1200 Subject: [PATCH 02/17] Replace stale audit evidence pointers --- .../AGENT_PROGRESS.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md index eff8b8f..e116127 100644 --- a/docs/open-collection-gap-analysis/AGENT_PROGRESS.md +++ b/docs/open-collection-gap-analysis/AGENT_PROGRESS.md @@ -57,13 +57,13 @@ Use full branch names for stacking existing branches with `but move