Skip to content

Commit 2001b15

Browse files
Alix-007vincentkoc
andauthored
fix(google-meet): bound Drive document export reads to prevent OOM (#97620)
* fix(google-meet): bound Drive document export reads to prevent OOM * test(google-meet): return full guarded fetch result --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
1 parent 1f0c6a6 commit 2001b15

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Google Meet tests cover bounded Drive document export response reads.
2+
import { describe, expect, it, vi } from "vitest";
3+
import { exportGoogleDriveDocumentText } from "./drive.js";
4+
5+
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
6+
fetchWithSsrFGuard: vi.fn(),
7+
}));
8+
9+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
10+
11+
const mockFetch = vi.mocked(fetchWithSsrFGuard);
12+
13+
function makeStreamResponse(sizeBytes: number, status = 200): Response {
14+
const chunk = new Uint8Array(Math.min(sizeBytes, 65536)).fill(0x78); // 'x'
15+
let sent = 0;
16+
const stream = new ReadableStream<Uint8Array>({
17+
pull(controller) {
18+
if (sent >= sizeBytes) {
19+
controller.close();
20+
return;
21+
}
22+
const remaining = sizeBytes - sent;
23+
const toSend = Math.min(chunk.length, remaining);
24+
controller.enqueue(chunk.subarray(0, toSend));
25+
sent += toSend;
26+
},
27+
});
28+
return new Response(stream, {
29+
status,
30+
headers: { "Content-Type": "text/plain" },
31+
});
32+
}
33+
34+
describe("exportGoogleDriveDocumentText bound", () => {
35+
it("returns document text when response is within the 16 MiB cap", async () => {
36+
const UNDER_CAP = 256;
37+
const response = makeStreamResponse(UNDER_CAP);
38+
mockFetch.mockResolvedValueOnce({
39+
response,
40+
finalUrl: "https://www.googleapis.com/drive/v3/files/doc-id/export?mimeType=text%2Fplain",
41+
release: vi.fn(async () => undefined),
42+
});
43+
44+
const result = await exportGoogleDriveDocumentText({
45+
accessToken: "tok",
46+
documentId: "doc-id",
47+
});
48+
49+
expect(typeof result).toBe("string");
50+
expect(result.length).toBeGreaterThan(0);
51+
});
52+
53+
it("rejects with a size error when response exceeds 16 MiB cap (fail-closed)", async () => {
54+
const OVER_CAP = 17 * 1024 * 1024; // 17 MiB
55+
const response = makeStreamResponse(OVER_CAP);
56+
const release = vi.fn(async () => undefined);
57+
mockFetch.mockResolvedValueOnce({
58+
response,
59+
finalUrl: "https://www.googleapis.com/drive/v3/files/doc-id/export?mimeType=text%2Fplain",
60+
release,
61+
});
62+
63+
await expect(
64+
exportGoogleDriveDocumentText({ accessToken: "tok", documentId: "doc-id" }),
65+
).rejects.toThrow(/exceeds/i);
66+
67+
expect(release).toHaveBeenCalledTimes(1);
68+
});
69+
70+
it("negative-control: bare response.text() buffers the full oversized body (no protection)", async () => {
71+
const OVER_CAP = 17 * 1024 * 1024; // 17 MiB
72+
const response = makeStreamResponse(OVER_CAP);
73+
// Calling response.text() directly buffers everything without throwing.
74+
const text = await response.text();
75+
expect(text.length).toBeGreaterThan(16 * 1024 * 1024);
76+
});
77+
});

extensions/google-meet/src/drive.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Google Meet plugin module implements drive behavior.
22
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
3+
import { readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
34
import { googleApiError } from "./google-api-errors.js";
45

56
const GOOGLE_DRIVE_API_BASE_URL = "https://www.googleapis.com/drive/v3";
@@ -64,7 +65,7 @@ export async function exportGoogleDriveDocumentText(params: {
6465
scopes: [GOOGLE_DRIVE_MEET_SCOPE],
6566
});
6667
}
67-
return await response.text();
68+
return await readProviderTextResponse(response, "Google Drive files.export");
6869
} finally {
6970
await release();
7071
}

0 commit comments

Comments
 (0)