Skip to content

Commit 0f775fa

Browse files
Alix-007steipete
andauthored
fix(msteams): bound Microsoft Graph API response reads in graph-upload to prevent OOM (#97784)
* fix(msteams): bound Microsoft Graph API response reads in graph-upload to prevent OOM * test(msteams): prove graph upload oversized JSON rejection * test(msteams): tighten Graph response bound proof --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 9cd29dc commit 0f775fa

3 files changed

Lines changed: 72 additions & 16 deletions

File tree

CHANGELOG.md

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

2424
### Fixes
2525

26+
- **Microsoft Teams Graph response bounds:** cap successful file-upload and chat JSON reads so oversized Microsoft Graph responses cannot be buffered without limit. (#97784) Thanks @Alix-007.
2627
- **Packaged speech runtime:** stop treating package-backed `speech-core` as a bundled plugin sidecar, restoring TTS startup in npm installs while release checks keep true activation-bypassing facades package-complete. (#89899, #89425) Thanks @zhangguiping-xydt.
2728
- **Codex app-server protocol:** require app-server 0.142 or newer, remove pre-0.142 wire-shape compatibility, and teach Codex to retrieve deferred native `spawn_agent` through `tool_search` so native subagent task mirroring works on search-capable models. (#101221)
2829
- **Android hardware keyboard chat:** send with unmodified Enter on physical keyboards while preserving Shift+Enter and other modified Enter combinations for multiline input. (#101239) Thanks @3ninyt3nin-creator.

extensions/msteams/src/graph-upload.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Msteams tests cover graph upload plugin behavior.
2-
import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
3-
import { describe, expect, it, vi } from "vitest";
2+
import { withFetchPreconnect, withServer } from "openclaw/plugin-sdk/test-env";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
44
import { buildTeamsFileInfoCard } from "./graph-chat.js";
55
import { resolveGraphChatId, uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js";
66

@@ -249,6 +249,60 @@ describe("resolveGraphChatId", () => {
249249
});
250250
});
251251

252+
describe("graph upload response limits", () => {
253+
const tokenProvider = {
254+
getAccessToken: vi.fn(async () => "graph-token"),
255+
};
256+
257+
afterEach(() => {
258+
vi.restoreAllMocks();
259+
vi.unstubAllGlobals();
260+
});
261+
262+
it("rejects an oversized upload response from a real loopback server", async () => {
263+
await withServer(
264+
(_req, res) => {
265+
res.writeHead(200, { "content-type": "application/json" });
266+
const chunk = Buffer.alloc(64 * 1024, 0x20);
267+
let remaining = 257; // >16 MiB when combined with the JSON prefix.
268+
res.write(
269+
'{"id":"item-big","webUrl":"https://example.com/big","name":"big.txt","padding":"',
270+
);
271+
const writeNext = () => {
272+
if (remaining <= 0) {
273+
res.end('"}');
274+
return;
275+
}
276+
remaining -= 1;
277+
if (res.write(chunk)) {
278+
setImmediate(writeNext);
279+
} else {
280+
res.once("drain", writeNext);
281+
}
282+
};
283+
writeNext();
284+
},
285+
async (baseUrl) => {
286+
const realFetch = globalThis.fetch.bind(globalThis);
287+
vi.stubGlobal(
288+
"fetch",
289+
withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
290+
const url = new URL(input instanceof Request ? input.url : String(input));
291+
const loopback = new URL(`${url.pathname}${url.search}`, baseUrl);
292+
return realFetch(loopback, init);
293+
}),
294+
);
295+
296+
await expect(
297+
uploadToOneDrive({ buffer: Buffer.from("x"), filename: "big.txt", tokenProvider }),
298+
).rejects.toThrow(
299+
"msteams.graph-upload.uploadOneDriveFile: JSON response exceeds 16777216 bytes",
300+
);
301+
},
302+
);
303+
});
304+
});
305+
252306
describe("buildTeamsFileInfoCard", () => {
253307
it("extracts a unique id from quoted etags and lowercases file extensions", () => {
254308
expect(

extensions/msteams/src/graph-upload.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* - Getting chat members for per-user sharing
1010
*/
1111

12+
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
1213
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
1314
import { createMSTeamsHttpError } from "./http-error.js";
1415
import { buildUserAgent } from "./user-agent.js";
@@ -54,11 +55,11 @@ export async function uploadToOneDrive(params: {
5455
throw await createMSTeamsHttpError(res, "OneDrive upload failed");
5556
}
5657

57-
const data = (await res.json()) as {
58+
const data = await readProviderJsonResponse<{
5859
id?: string;
5960
webUrl?: string;
6061
name?: string;
61-
};
62+
}>(res, "msteams.graph-upload.uploadOneDriveFile");
6263

6364
if (!data.id || !data.webUrl || !data.name) {
6465
throw new Error("OneDrive upload response missing required fields");
@@ -106,9 +107,9 @@ async function createSharingLink(params: {
106107
throw await createMSTeamsHttpError(res, "Create sharing link failed");
107108
}
108109

109-
const data = (await res.json()) as {
110+
const data = await readProviderJsonResponse<{
110111
link?: { webUrl?: string };
111-
};
112+
}>(res, "msteams.graph-upload.createOneDriveSharingLink");
112113

113114
if (!data.link?.webUrl) {
114115
throw new Error("Create sharing link response missing webUrl");
@@ -200,11 +201,11 @@ export async function uploadToSharePoint(params: {
200201
throw await createMSTeamsHttpError(res, "SharePoint upload failed");
201202
}
202203

203-
const data = (await res.json()) as {
204+
const data = await readProviderJsonResponse<{
204205
id?: string;
205206
webUrl?: string;
206207
name?: string;
207-
};
208+
}>(res, "msteams.graph-upload.uploadSharePointFile");
208209

209210
if (!data.id || !data.webUrl || !data.name) {
210211
throw new Error("SharePoint upload response missing required fields");
@@ -260,11 +261,11 @@ export async function getDriveItemProperties(params: {
260261
throw await createMSTeamsHttpError(res, "Get driveItem properties failed");
261262
}
262263

263-
const data = (await res.json()) as {
264+
const data = await readProviderJsonResponse<{
264265
eTag?: string;
265266
webDavUrl?: string;
266267
name?: string;
267-
};
268+
}>(res, "msteams.graph-upload.getDriveItemProperties");
268269

269270
if (!data.eTag || !data.webDavUrl || !data.name) {
270271
throw new Error("DriveItem response missing required properties (eTag, webDavUrl, or name)");
@@ -331,9 +332,9 @@ export async function resolveGraphChatId(params: {
331332
return null;
332333
}
333334

334-
const data = (await res.json()) as {
335+
const data = await readProviderJsonResponse<{
335336
value?: Array<{ id?: string }>;
336-
};
337+
}>(res, "msteams.graph-upload.getOneOnOneChatId");
337338

338339
const chats = data.value ?? [];
339340

@@ -371,12 +372,12 @@ async function getChatMembers(params: {
371372
throw await createMSTeamsHttpError(res, "Get chat members failed");
372373
}
373374

374-
const data = (await res.json()) as {
375+
const data = await readProviderJsonResponse<{
375376
value?: Array<{
376377
userId?: string;
377378
displayName?: string;
378379
}>;
379-
};
380+
}>(res, "msteams.graph-upload.getChatMembers");
380381

381382
return (data.value ?? [])
382383
.map((m) => ({
@@ -435,9 +436,9 @@ async function createSharePointSharingLink(params: {
435436
throw await createMSTeamsHttpError(res, "Create SharePoint sharing link failed");
436437
}
437438

438-
const data = (await res.json()) as {
439+
const data = await readProviderJsonResponse<{
439440
link?: { webUrl?: string };
440-
};
441+
}>(res, "msteams.graph-upload.createSharePointSharingLink");
441442

442443
if (!data.link?.webUrl) {
443444
throw new Error("Create SharePoint sharing link response missing webUrl");

0 commit comments

Comments
 (0)