mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
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:
parent
9cd29dcb9a
commit
0f775fa25e
3 changed files with 72 additions and 16 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue