fix(slack): guard relay frame parsing

* 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 <liao.shirong@xydigit.com>

* fix(slack): repair relay frame guard tests

---------

Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
lsr911
2026-07-01 19:50:08 +08:00
committed by GitHub
parent 60d55a1725
commit badbae77df
2 changed files with 54 additions and 2 deletions

View File

@@ -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<T>() {
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]);
});
});
});

View File

@@ -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 {