Skip to content

Commit 24330a8

Browse files
fix(mattermost): reject oversized websocket events (#99366)
* fix(mattermost): bound monitor websocket payloads * fix(mattermost): preserve websocket payload headroom Co-authored-by: sunlit-deng <[email protected]> --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent cc2c6a8 commit 24330a8

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

extensions/mattermost/src/mattermost/monitor-websocket.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Mattermost tests cover monitor websocket plugin behavior.
2+
import { once } from "node:events";
23
import { beforeEach, describe, expect, it, vi } from "vitest";
4+
import { WebSocketServer } from "ws";
35
import type { RuntimeEnv } from "../../runtime-api.js";
46
import {
57
createMattermostConnectOnce,
8+
MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES,
69
type MattermostWebSocketLike,
710
WebSocketClosedBeforeOpenError,
811
} from "./monitor-websocket.js";
@@ -194,6 +197,78 @@ describe("mattermost websocket monitor", () => {
194197
expect(countMatching(patches, (patch) => patch.connected === false)).toBe(2);
195198
});
196199

200+
it("accepts large valid post envelopes and rejects oversized websocket payloads", async () => {
201+
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
202+
await once(server, "listening");
203+
const address = server.address();
204+
if (!address || typeof address === "string") {
205+
throw new Error("expected TCP websocket server address");
206+
}
207+
208+
const quotedCardBody = '"'.repeat(380_000);
209+
const largeProps = { cards: [{ body: quotedCardBody }] };
210+
const largePostEnvelope = JSON.stringify({
211+
event: "posted",
212+
data: {
213+
post: JSON.stringify({
214+
id: "post-large",
215+
message: "large Mattermost integration post",
216+
props: largeProps,
217+
}),
218+
},
219+
});
220+
expect(JSON.stringify(largeProps).length).toBeLessThan(800_000);
221+
expect(Buffer.byteLength(largePostEnvelope)).toBeGreaterThan(1024 * 1024);
222+
expect(Buffer.byteLength(largePostEnvelope)).toBeLessThan(
223+
MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES,
224+
);
225+
226+
const runtime = testRuntime();
227+
const onPosted = vi.fn(async () => {});
228+
server.on("connection", (socket) => {
229+
socket.once("message", () => {
230+
socket.send(
231+
JSON.stringify({
232+
event: "posted",
233+
data: {
234+
post: JSON.stringify({
235+
id: "post-1",
236+
message: "normal Mattermost post",
237+
}),
238+
},
239+
}),
240+
);
241+
socket.send(largePostEnvelope);
242+
socket.send(Buffer.alloc(MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES + 1, 0x78));
243+
});
244+
});
245+
246+
try {
247+
await createMattermostConnectOnce({
248+
wsUrl: `ws://127.0.0.1:${address.port}`,
249+
botToken: "token",
250+
runtime,
251+
nextSeq: () => 1,
252+
onPosted,
253+
})();
254+
} finally {
255+
server.close();
256+
await once(server, "close");
257+
}
258+
259+
expect(onPosted).toHaveBeenCalledWith(
260+
expect.objectContaining({ id: "post-1", message: "normal Mattermost post" }),
261+
expect.any(Object),
262+
);
263+
expect(onPosted).toHaveBeenCalledWith(
264+
expect.objectContaining({ id: "post-large", props: largeProps }),
265+
expect.any(Object),
266+
);
267+
expect(runtime.error).toHaveBeenCalledWith(
268+
expect.stringContaining("Max payload size exceeded"),
269+
);
270+
});
271+
197272
it("dispatches reaction events to the reaction handler", async () => {
198273
const socket = new FakeWebSocket();
199274
const onPosted = vi.fn(async () => {});

extensions/mattermost/src/mattermost/monitor-websocket.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export type MattermostWebSocketLike = {
4444
};
4545

4646
export type MattermostWebSocketFactory = (url: string) => MattermostWebSocketLike;
47+
// Mattermost events can include double-encoded post props plus server/plugin metadata.
48+
// Keep channel-compatible headroom while bounding ws's 100 MiB default before parsing.
49+
export const MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES = 16 * 1024 * 1024;
4750
const MattermostEventPayloadSchema = z.object({
4851
event: z.string().optional(),
4952
data: z
@@ -113,7 +116,10 @@ type CreateMattermostConnectOnceOpts = {
113116

114117
const defaultMattermostWebSocketFactory: MattermostWebSocketFactory = (url) => {
115118
const agent = createDebugProxyWebSocketAgent(resolveDebugProxySettings());
116-
return new WebSocket(url, agent ? { agent } : undefined) as MattermostWebSocketLike;
119+
return new WebSocket(url, {
120+
...(agent ? { agent } : {}),
121+
maxPayload: MATTERMOST_WEBSOCKET_MAX_PAYLOAD_BYTES,
122+
}) as MattermostWebSocketLike;
117123
};
118124

119125
function parsePostedPayload(

0 commit comments

Comments
 (0)