From badbae77dfb85e1fb0fd4fd32a58d53a7140789c Mon Sep 17 00:00:00 2001 From: lsr911 Date: Wed, 1 Jul 2026 19:50:08 +0800 Subject: [PATCH] fix(slack): guard relay frame parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(slack): guard relay WebSocket frame JSON.parse against malformed input Slack Socket Mode relay receives WebSocket frames from external infrastructure. parseRelayFrame() used bare JSON.parse() which would throw raw SyntaxError on malformed frames, potentially crashing the relay connection. Wrap JSON.parse in try/catch and throw SlackRelayMalformedFrameError with the original SyntaxError as cause, so callers can distinguish transport errors from parse errors. D4 dimension — all competitors have zero coverage. Signed-off-by: lsr911 * fix(slack): repair relay frame guard tests --------- Signed-off-by: lsr911 Co-authored-by: Vincent Koc --- .../slack/src/monitor/relay-source.test.ts | 41 +++++++++++++++++++ extensions/slack/src/monitor/relay-source.ts | 15 ++++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/extensions/slack/src/monitor/relay-source.test.ts b/extensions/slack/src/monitor/relay-source.test.ts index 8b15c60a4572..503526282c56 100644 --- a/extensions/slack/src/monitor/relay-source.test.ts +++ b/extensions/slack/src/monitor/relay-source.test.ts @@ -6,6 +6,8 @@ import { buildRelayWebSocketOptions, buildRelayWebSocketUrl, monitorSlackRelaySource, + parseRelayFrame, + SlackRelayMalformedFrameError, SLACK_RELAY_MAX_PAYLOAD_BYTES, type SlackRelayIdentity, } from "./relay-source.js"; @@ -20,6 +22,10 @@ function deferred() { return { promise, reject, resolve }; } +function relayFrame(text: string): Buffer { + return Buffer.from(text, "utf8"); +} + describe("Slack relay source", () => { it("builds authenticated relay websocket URLs safely", () => { expect( @@ -198,4 +204,39 @@ describe("Slack relay source", () => { }); }); }); + + describe("parseRelayFrame", () => { + it("parses valid JSON frames", () => { + const frame = parseRelayFrame( + relayFrame(JSON.stringify({ type: "slack_event", data: { text: "hello" } })), + ); + expect(frame).toEqual({ type: "slack_event", data: { text: "hello" } }); + }); + + it("throws SlackRelayMalformedFrameError for malformed JSON", () => { + expect(() => parseRelayFrame(relayFrame("NOT JSON {{{"))).toThrow( + SlackRelayMalformedFrameError, + ); + }); + + it("wraps the original SyntaxError as the cause", () => { + let error: unknown; + try { + parseRelayFrame(relayFrame("NOT JSON {{{")); + } catch (err: unknown) { + error = err; + } + expect(error).toBeInstanceOf(SlackRelayMalformedFrameError); + expect((error as SlackRelayMalformedFrameError).message).toContain("malformed JSON frame"); + expect((error as SlackRelayMalformedFrameError).cause).toBeDefined(); + }); + + it("parses empty object frames", () => { + expect(parseRelayFrame(relayFrame("{}"))).toEqual({}); + }); + + it("parses array frames", () => { + expect(parseRelayFrame(relayFrame("[1, 2, 3]"))).toEqual([1, 2, 3]); + }); + }); }); diff --git a/extensions/slack/src/monitor/relay-source.ts b/extensions/slack/src/monitor/relay-source.ts index 7ef7ba75341c..6dd118c4aa21 100644 --- a/extensions/slack/src/monitor/relay-source.ts +++ b/extensions/slack/src/monitor/relay-source.ts @@ -293,9 +293,20 @@ function isLocalRelayHost(hostname: string): boolean { return isIP(host) === 4 && host.startsWith("127."); } -function parseRelayFrame(data: RawData): unknown { +export class SlackRelayMalformedFrameError extends Error { + constructor(message: string, options?: ErrorOptions) { + super(message, options); + this.name = "SlackRelayMalformedFrameError"; + } +} + +export function parseRelayFrame(data: RawData): unknown { const text = rawDataToString(data); - return JSON.parse(text) as unknown; + try { + return JSON.parse(text) as unknown; + } catch (cause) { + throw new SlackRelayMalformedFrameError("Slack relay received malformed JSON frame", { cause }); + } } function rawDataToString(data: RawData): string {