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 <steipete@gmail.com>
This commit is contained in:
Alix-007 2026-07-07 17:36:54 +08:00 committed by GitHub
parent 9cd29dcb9a
commit 0f775fa25e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 72 additions and 16 deletions

View file

@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- **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.
- **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.
- **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)
- **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.

View file

@ -1,6 +1,6 @@
// Msteams tests cover graph upload plugin behavior.
import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
import { describe, expect, it, vi } from "vitest";
import { withFetchPreconnect, withServer } from "openclaw/plugin-sdk/test-env";
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildTeamsFileInfoCard } from "./graph-chat.js";
import { resolveGraphChatId, uploadToOneDrive, uploadToSharePoint } from "./graph-upload.js";
@ -249,6 +249,60 @@ describe("resolveGraphChatId", () => {
});
});
describe("graph upload response limits", () => {
const tokenProvider = {
getAccessToken: vi.fn(async () => "graph-token"),
};
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it("rejects an oversized upload response from a real loopback server", async () => {
await withServer(
(_req, res) => {
res.writeHead(200, { "content-type": "application/json" });
const chunk = Buffer.alloc(64 * 1024, 0x20);
let remaining = 257; // >16 MiB when combined with the JSON prefix.
res.write(
'{"id":"item-big","webUrl":"https://example.com/big","name":"big.txt","padding":"',
);
const writeNext = () => {
if (remaining <= 0) {
res.end('"}');
return;
}
remaining -= 1;
if (res.write(chunk)) {
setImmediate(writeNext);
} else {
res.once("drain", writeNext);
}
};
writeNext();
},
async (baseUrl) => {
const realFetch = globalThis.fetch.bind(globalThis);
vi.stubGlobal(
"fetch",
withFetchPreconnect(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = new URL(input instanceof Request ? input.url : String(input));
const loopback = new URL(`${url.pathname}${url.search}`, baseUrl);
return realFetch(loopback, init);
}),
);
await expect(
uploadToOneDrive({ buffer: Buffer.from("x"), filename: "big.txt", tokenProvider }),
).rejects.toThrow(
"msteams.graph-upload.uploadOneDriveFile: JSON response exceeds 16777216 bytes",
);
},
);
});
});
describe("buildTeamsFileInfoCard", () => {
it("extracts a unique id from quoted etags and lowercases file extensions", () => {
expect(

View file

@ -9,6 +9,7 @@
* - Getting chat members for per-user sharing
*/
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
import { createMSTeamsHttpError } from "./http-error.js";
import { buildUserAgent } from "./user-agent.js";
@ -54,11 +55,11 @@ export async function uploadToOneDrive(params: {
throw await createMSTeamsHttpError(res, "OneDrive upload failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
id?: string;
webUrl?: string;
name?: string;
};
}>(res, "msteams.graph-upload.uploadOneDriveFile");
if (!data.id || !data.webUrl || !data.name) {
throw new Error("OneDrive upload response missing required fields");
@ -106,9 +107,9 @@ async function createSharingLink(params: {
throw await createMSTeamsHttpError(res, "Create sharing link failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
link?: { webUrl?: string };
};
}>(res, "msteams.graph-upload.createOneDriveSharingLink");
if (!data.link?.webUrl) {
throw new Error("Create sharing link response missing webUrl");
@ -200,11 +201,11 @@ export async function uploadToSharePoint(params: {
throw await createMSTeamsHttpError(res, "SharePoint upload failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
id?: string;
webUrl?: string;
name?: string;
};
}>(res, "msteams.graph-upload.uploadSharePointFile");
if (!data.id || !data.webUrl || !data.name) {
throw new Error("SharePoint upload response missing required fields");
@ -260,11 +261,11 @@ export async function getDriveItemProperties(params: {
throw await createMSTeamsHttpError(res, "Get driveItem properties failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
eTag?: string;
webDavUrl?: string;
name?: string;
};
}>(res, "msteams.graph-upload.getDriveItemProperties");
if (!data.eTag || !data.webDavUrl || !data.name) {
throw new Error("DriveItem response missing required properties (eTag, webDavUrl, or name)");
@ -331,9 +332,9 @@ export async function resolveGraphChatId(params: {
return null;
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
value?: Array<{ id?: string }>;
};
}>(res, "msteams.graph-upload.getOneOnOneChatId");
const chats = data.value ?? [];
@ -371,12 +372,12 @@ async function getChatMembers(params: {
throw await createMSTeamsHttpError(res, "Get chat members failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
value?: Array<{
userId?: string;
displayName?: string;
}>;
};
}>(res, "msteams.graph-upload.getChatMembers");
return (data.value ?? [])
.map((m) => ({
@ -435,9 +436,9 @@ async function createSharePointSharingLink(params: {
throw await createMSTeamsHttpError(res, "Create SharePoint sharing link failed");
}
const data = (await res.json()) as {
const data = await readProviderJsonResponse<{
link?: { webUrl?: string };
};
}>(res, "msteams.graph-upload.createSharePointSharingLink");
if (!data.link?.webUrl) {
throw new Error("Create SharePoint sharing link response missing webUrl");