From e258fdae99c090c92855bae5226dbaf0dabebb9f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 14:19:05 +0000 Subject: [PATCH] fix(peer): run isPeerMessage in the send message guards isClientPeerSendMessage and isServerPeerSendMessage only checked kind and json, assuming isPeerMessage had already run, so a caller validating raw input with them alone accepted a non-string id or a non-binary binary, and null threw a TypeError. They now take unknown and run isPeerMessage first, so they are safe on untrusted input. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01X6CM5zP1QBSVRmibkRqzK3 --- packages/peer/src/validators.test.ts | 18 ++++++++++++++++++ packages/peer/src/validators.ts | 24 +++++++++++++----------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/peer/src/validators.test.ts b/packages/peer/src/validators.test.ts index ce37a332..c3af6c91 100644 --- a/packages/peer/src/validators.test.ts +++ b/packages/peer/src/validators.test.ts @@ -254,6 +254,15 @@ describe('isClientPeerSendMessage', () => { ])('rejects %s', (_, overrides) => { expect(isClientPeerSendMessage(base(overrides))).toBe(false) }) + + it.each([ + ['non-string id', base({ id: 123 as any, kind: 'request', json: { url: '/path' } })], + ['binary as string', base({ kind: 'octet-stream', json: {}, binary: 'data' as any })], + ['null', null], + ['array', []], + ])('rejects %s, which is not a peer message', (_, v) => { + expect(isClientPeerSendMessage(v)).toBe(false) + }) }) describe('isServerPeerSendMessage', () => { @@ -270,4 +279,13 @@ describe('isServerPeerSendMessage', () => { it('rejects request', () => { expect(isServerPeerSendMessage(base({ kind: 'request', json: { url: '/path' } }))).toBe(false) }) + + it.each([ + ['non-string id', base({ id: 123 as any, kind: 'response', json: {} })], + ['binary as string', base({ kind: 'response', json: {}, binary: 'data' as any })], + ['null', null], + ['array', []], + ])('rejects %s, which is not a peer message', (_, v) => { + expect(isServerPeerSendMessage(v)).toBe(false) + }) }) diff --git a/packages/peer/src/validators.ts b/packages/peer/src/validators.ts index 778b0eb4..96874d8b 100644 --- a/packages/peer/src/validators.ts +++ b/packages/peer/src/validators.ts @@ -137,21 +137,23 @@ export function isPeerStreamCancelMessage(maybe: PeerMessage): maybe is PeerStre return maybe.kind === 'stream/cancel' && maybe.json === undefined && maybe.binary === undefined } -export function isClientPeerSendMessage(maybe: PeerMessage): maybe is ClientPeerSendMessage { +export function isClientPeerSendMessage(maybe: unknown): maybe is ClientPeerSendMessage { return ( - isPeerRequestMessage(maybe) || - isPeerCancelMessage(maybe) || - isPeerEventStreamMessage(maybe) || - isPeerOctetStreamMessage(maybe) + isPeerMessage(maybe) && + (isPeerRequestMessage(maybe) || + isPeerCancelMessage(maybe) || + isPeerEventStreamMessage(maybe) || + isPeerOctetStreamMessage(maybe)) ) } -export function isServerPeerSendMessage(maybe: PeerMessage): maybe is ServerPeerSendMessage { +export function isServerPeerSendMessage(maybe: unknown): maybe is ServerPeerSendMessage { return ( - isPeerResponseMessage(maybe) || - isPeerCancelMessage(maybe) || - isPeerEventStreamMessage(maybe) || - isPeerOctetStreamMessage(maybe) || - isPeerStreamCancelMessage(maybe) + isPeerMessage(maybe) && + (isPeerResponseMessage(maybe) || + isPeerCancelMessage(maybe) || + isPeerEventStreamMessage(maybe) || + isPeerOctetStreamMessage(maybe) || + isPeerStreamCancelMessage(maybe)) ) }