Skip to content

Commit 82ccc3f

Browse files
committed
fix: harden msteams webhook ingress lifecycle integration (#25960) (thanks @bmendonca3)
1 parent 9c804a4 commit 82ccc3f

4 files changed

Lines changed: 32 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai
4747

4848
### Fixes
4949

50+
- MSTeams/webhook ingress hardening: apply explicit HTTP server timeouts, run auth middleware before JSON body parsing, and keep timeout/lifecycle wiring compatible with gateway-managed monitor startup. (#25960) Thanks @bmendonca3.
5051
- Synology Chat/webhook compatibility: accept JSON and alias payload fields, allow token resolution from body/query/header sources, and ACK webhook requests with `204` to avoid persistent `Processing...` states in Synology Chat clients. (#26635) Thanks @memphislee09-source.
5152
- Synology Chat/webhook ingress hardening: enforce bounded body reads (size + timeout) via shared request-body guards to prevent unauthenticated slow-body hangs before token validation. (#25831) Thanks @bmendonca3.
5253
- Synology Chat/reply delivery: resolve webhook usernames to Chat API `user_id` values for outbound chatbot replies, avoiding mismatches between webhook user IDs and `method=chatbot` recipient IDs in multi-account setups. (#23709) Thanks @druide67.

extensions/msteams/src/monitor.lifecycle.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import type { MSTeamsPollStore } from "./polls.js";
66

77
type FakeServer = EventEmitter & {
88
close: (callback?: (err?: Error | null) => void) => void;
9+
setTimeout: (msecs: number, callback?: () => void) => FakeServer;
10+
timeout: number;
11+
requestTimeout: number;
12+
headersTimeout: number;
913
};
1014

1115
const expressControl = vi.hoisted(() => ({
@@ -31,6 +35,13 @@ vi.mock("express", () => {
3135
post: vi.fn(),
3236
listen: vi.fn((_port: number) => {
3337
const server = new EventEmitter() as FakeServer;
38+
server.timeout = 0;
39+
server.requestTimeout = 0;
40+
server.headersTimeout = 0;
41+
server.setTimeout = vi.fn((msecs: number) => {
42+
server.timeout = msecs;
43+
return server;
44+
});
3445
server.close = (callback?: (err?: Error | null) => void) => {
3546
queueMicrotask(() => {
3647
server.emit("close");

extensions/msteams/src/monitor.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ async function waitForSlowBodySocketClose(port: number, timeoutMs: number): Prom
3737
}
3838

3939
describe("msteams monitor webhook hardening", () => {
40-
it("applies explicit webhook timeout values", async () => {
40+
it("applies timeout values and clamps headersTimeout to requestTimeout", async () => {
4141
const app = express();
4242
const server = app.listen(0, "127.0.0.1");
4343
await once(server, "listening");

extensions/msteams/src/monitor.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export async function monitorMSTeamsProvider(
268268

269269
// Create Express server
270270
const expressApp = express.default();
271+
expressApp.use(authorizeJWT(authConfig));
271272
expressApp.use(express.json({ limit: MSTEAMS_WEBHOOK_MAX_BODY_BYTES }));
272273
expressApp.use((err: unknown, _req: Request, res: Response, next: (err?: unknown) => void) => {
273274
if (err && typeof err === "object" && "status" in err && err.status === 413) {
@@ -276,7 +277,6 @@ export async function monitorMSTeamsProvider(
276277
}
277278
next(err);
278279
});
279-
expressApp.use(authorizeJWT(authConfig));
280280

281281
// Set up the messages endpoint - use configured path and /api/messages as fallback
282282
const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages";
@@ -301,9 +301,9 @@ export async function monitorMSTeamsProvider(
301301

302302
// Start listening and fail fast if bind/listen fails.
303303
const httpServer = expressApp.listen(port);
304+
applyMSTeamsWebhookTimeouts(httpServer);
304305
await new Promise<void>((resolve, reject) => {
305306
const onListening = () => {
306-
httpServer.off("error", onError);
307307
log.info(`msteams provider started on port ${port}`);
308308
resolve();
309309
};
@@ -315,8 +315,6 @@ export async function monitorMSTeamsProvider(
315315
httpServer.once("listening", onListening);
316316
httpServer.once("error", onError);
317317
});
318-
applyMSTeamsWebhookTimeouts(httpServer);
319-
320318
httpServer.on("error", (err) => {
321319
log.error("msteams server error", { error: String(err) });
322320
});
@@ -333,24 +331,30 @@ export async function monitorMSTeamsProvider(
333331
});
334332
};
335333

334+
// Some direct callers may invoke monitor without lifecycle wiring.
335+
// Return immediately so they can call shutdown explicitly.
336+
if (!opts.abortSignal) {
337+
return { app: expressApp, shutdown };
338+
}
339+
340+
const closePromise = new Promise<void>((resolve) => {
341+
httpServer.once("close", () => {
342+
resolve();
343+
});
344+
});
345+
336346
// Handle abort signal
337347
const onAbort = () => {
338348
void shutdown();
339349
};
340-
if (opts.abortSignal) {
341-
if (opts.abortSignal.aborted) {
342-
onAbort();
343-
} else {
344-
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
345-
}
350+
if (opts.abortSignal.aborted) {
351+
onAbort();
352+
} else {
353+
opts.abortSignal.addEventListener("abort", onAbort, { once: true });
346354
}
347355

348356
// Keep this task alive until shutdown/close so gateway runtime does not treat startup as exit.
349-
await new Promise<void>((resolve) => {
350-
httpServer.once("close", () => {
351-
resolve();
352-
});
353-
});
357+
await closePromise;
354358
opts.abortSignal?.removeEventListener("abort", onAbort);
355359

356360
return { app: expressApp, shutdown };

0 commit comments

Comments
 (0)