High severity7.5NVD Advisory· Published Apr 28, 2026· Updated Apr 30, 2026
CVE-2026-41405
CVE-2026-41405
Description
OpenClaw before 2026.3.31 parses MS Teams webhook request bodies before performing JWT validation, allowing unauthenticated attackers to trigger resource exhaustion. Remote attackers can send malicious Teams webhook payloads to exhaust server resources by bypassing authentication checks.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.3.31 | 2026.3.31 |
Affected products
1Patches
13834d47099ddMS Teams: validate webhook auth before JSON parsing (#57686)
2 files changed · +74 −13
extensions/msteams/src/monitor.lifecycle.test.ts+62 −2 modified@@ -1,4 +1,5 @@ import { EventEmitter } from "node:events"; +import type { Request, Response } from "express"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig, RuntimeEnv } from "../runtime-api.js"; import type { MSTeamsConversationStore } from "./conversation-store.js"; @@ -13,6 +14,11 @@ type FakeServer = EventEmitter & { const expressControl = vi.hoisted(() => ({ mode: { value: "listening" as "listening" | "error" }, + apps: [] as Array<{ + use: ReturnType<typeof vi.fn>; + post: ReturnType<typeof vi.fn>; + listen: ReturnType<typeof vi.fn>; + }>, })); vi.mock("../runtime-api.js", () => ({ @@ -72,8 +78,14 @@ vi.mock("express", () => { }), }); + const wrappedFactory = () => { + const app = factory(); + expressControl.apps.push(app); + return app; + }; + return { - default: factory, + default: wrappedFactory, json, }; }); @@ -88,6 +100,7 @@ const createMSTeamsAdapter = vi.hoisted(() => process: vi.fn(async () => {}), })), ); +const jwtValidate = vi.hoisted(() => vi.fn().mockResolvedValue(true)); const loadMSTeamsSdkWithAuth = vi.hoisted(() => vi.fn(async () => ({ sdk: { @@ -117,7 +130,7 @@ vi.mock("./sdk.js", () => ({ getAccessToken: vi.fn().mockResolvedValue("mock-token"), }), createBotFrameworkJwtValidator: vi.fn().mockResolvedValue({ - validate: vi.fn().mockResolvedValue(true), + validate: jwtValidate, }), })); @@ -178,6 +191,8 @@ describe("monitorMSTeamsProvider lifecycle", () => { afterEach(() => { vi.clearAllMocks(); expressControl.mode.value = "listening"; + expressControl.apps.length = 0; + jwtValidate.mockReset().mockResolvedValue(true); }); it("stays active until aborted", async () => { @@ -215,4 +230,49 @@ describe("monitorMSTeamsProvider lifecycle", () => { }), ).rejects.toThrow(/EADDRINUSE/); }); + + it("runs JWT validation before JSON body parsing", async () => { + const abort = new AbortController(); + const task = monitorMSTeamsProvider({ + cfg: createConfig(0), + runtime: createRuntime(), + abortSignal: abort.signal, + conversationStore: createStores().conversationStore, + pollStore: createStores().pollStore, + }); + + await new Promise<void>((resolve) => setTimeout(resolve, 0)); + + const app = expressControl.apps.at(-1); + expect(app).toBeDefined(); + expect(app!.use).toHaveBeenCalledTimes(4); + + const jsonMiddleware = vi.mocked((await import("express")).json).mock.results[0]?.value; + expect(jsonMiddleware).toBeDefined(); + expect(app!.use.mock.calls[1]?.[0]).not.toBe(jsonMiddleware); + expect(app!.use.mock.calls[2]?.[0]).toBe(jsonMiddleware); + + const jwtMiddleware = app!.use.mock.calls[1]?.[0] as ( + req: Request, + res: Response, + next: (err?: unknown) => void, + ) => void; + const next = vi.fn(); + jwtMiddleware( + { headers: { authorization: "Bearer token" } } as Request, + { + status: vi.fn().mockReturnThis(), + json: vi.fn(), + } as unknown as Response, + next, + ); + + await vi.waitFor(() => { + expect(jwtValidate).toHaveBeenCalledWith("Bearer token"); + expect(next).toHaveBeenCalledTimes(1); + }); + + abort.abort(); + await task; + }); });
extensions/msteams/src/monitor.ts+12 −11 modified@@ -266,24 +266,16 @@ export async function monitorMSTeamsProvider( next(); }); - expressApp.use(express.json({ limit: MSTEAMS_WEBHOOK_MAX_BODY_BYTES })); - expressApp.use((err: unknown, _req: Request, res: Response, next: (err?: unknown) => void) => { - if (err && typeof err === "object" && "status" in err && err.status === 413) { - res.status(413).json({ error: "Payload too large" }); - return; - } - next(err); - }); - // JWT validation — verify Bot Framework tokens using the Teams SDK's // JwtValidator (validates signature via JWKS, audience, issuer, expiration). const jwtValidator = await createBotFrameworkJwtValidator(creds); expressApp.use((req: Request, res: Response, next: (err?: unknown) => void) => { // Authorization header is guaranteed by the pre-parse auth gate above. + // `serviceUrl` is optional, so authenticate from headers alone before body + // I/O to avoid spending memory and CPU on unauthenticated requests. const authHeader = req.headers.authorization!; - const serviceUrl = (req.body as Record<string, unknown>)?.serviceUrl as string | undefined; jwtValidator - .validate(authHeader, serviceUrl) + .validate(authHeader) .then((valid) => { if (!valid) { log.debug?.("JWT validation failed"); @@ -298,6 +290,15 @@ export async function monitorMSTeamsProvider( }); }); + expressApp.use(express.json({ limit: MSTEAMS_WEBHOOK_MAX_BODY_BYTES })); + expressApp.use((err: unknown, _req: Request, res: Response, next: (err?: unknown) => void) => { + if (err && typeof err === "object" && "status" in err && err.status === 413) { + res.status(413).json({ error: "Payload too large" }); + return; + } + next(err); + }); + // Set up the messages endpoint - use configured path and /api/messages as fallback const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages"; const messageHandler = (req: Request, res: Response) => {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/openclaw/openclaw/commit/3834d47099dd13c8244ed6de8b9ea9855c553623nvdPatchWEB
- github.com/advisories/GHSA-p464-m8x6-vhv8ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-p464-m8x6-vhv8nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-41405ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-resource-exhaustion-via-unauthenticated-ms-teams-webhook-body-parsingnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/releases/tag/v2026.3.31ghsaWEB
News mentions
0No linked articles in our index yet.