From 7998802f5ae7952ee8a0a28f5eca7e4984a7a5de Mon Sep 17 00:00:00 2001
From: Alix-007
Date: Mon, 29 Jun 2026 10:57:01 +0800
Subject: [PATCH] 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
(cherry picked from commit 2001b15f5b92d653464cbd847c28c136bdb465a7)
---
extensions/google-meet/src/drive.test.ts | 77 ++++++++++++++++++++++++
extensions/google-meet/src/drive.ts | 3 +-
2 files changed, 79 insertions(+), 1 deletion(-)
create mode 100644 extensions/google-meet/src/drive.test.ts
diff --git a/extensions/google-meet/src/drive.test.ts b/extensions/google-meet/src/drive.test.ts
new file mode 100644
index 000000000000..70d9534f2293
--- /dev/null
+++ b/extensions/google-meet/src/drive.test.ts
@@ -0,0 +1,77 @@
+// Google Meet tests cover bounded Drive document export response reads.
+import { describe, expect, it, vi } from "vitest";
+import { exportGoogleDriveDocumentText } from "./drive.js";
+
+vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
+ fetchWithSsrFGuard: vi.fn(),
+}));
+
+import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
+
+const mockFetch = vi.mocked(fetchWithSsrFGuard);
+
+function makeStreamResponse(sizeBytes: number, status = 200): Response {
+ const chunk = new Uint8Array(Math.min(sizeBytes, 65536)).fill(0x78); // 'x'
+ let sent = 0;
+ const stream = new ReadableStream({
+ pull(controller) {
+ if (sent >= sizeBytes) {
+ controller.close();
+ return;
+ }
+ const remaining = sizeBytes - sent;
+ const toSend = Math.min(chunk.length, remaining);
+ controller.enqueue(chunk.subarray(0, toSend));
+ sent += toSend;
+ },
+ });
+ return new Response(stream, {
+ status,
+ headers: { "Content-Type": "text/plain" },
+ });
+}
+
+describe("exportGoogleDriveDocumentText bound", () => {
+ it("returns document text when response is within the 16 MiB cap", async () => {
+ const UNDER_CAP = 256;
+ const response = makeStreamResponse(UNDER_CAP);
+ mockFetch.mockResolvedValueOnce({
+ response,
+ finalUrl: "https://www.googleapis.com/drive/v3/files/doc-id/export?mimeType=text%2Fplain",
+ release: vi.fn(async () => undefined),
+ });
+
+ const result = await exportGoogleDriveDocumentText({
+ accessToken: "tok",
+ documentId: "doc-id",
+ });
+
+ expect(typeof result).toBe("string");
+ expect(result.length).toBeGreaterThan(0);
+ });
+
+ it("rejects with a size error when response exceeds 16 MiB cap (fail-closed)", async () => {
+ const OVER_CAP = 17 * 1024 * 1024; // 17 MiB
+ const response = makeStreamResponse(OVER_CAP);
+ const release = vi.fn(async () => undefined);
+ mockFetch.mockResolvedValueOnce({
+ response,
+ finalUrl: "https://www.googleapis.com/drive/v3/files/doc-id/export?mimeType=text%2Fplain",
+ release,
+ });
+
+ await expect(
+ exportGoogleDriveDocumentText({ accessToken: "tok", documentId: "doc-id" }),
+ ).rejects.toThrow(/exceeds/i);
+
+ expect(release).toHaveBeenCalledTimes(1);
+ });
+
+ it("negative-control: bare response.text() buffers the full oversized body (no protection)", async () => {
+ const OVER_CAP = 17 * 1024 * 1024; // 17 MiB
+ const response = makeStreamResponse(OVER_CAP);
+ // Calling response.text() directly buffers everything without throwing.
+ const text = await response.text();
+ expect(text.length).toBeGreaterThan(16 * 1024 * 1024);
+ });
+});
diff --git a/extensions/google-meet/src/drive.ts b/extensions/google-meet/src/drive.ts
index a47da9188247..396f62c8517d 100644
--- a/extensions/google-meet/src/drive.ts
+++ b/extensions/google-meet/src/drive.ts
@@ -1,5 +1,6 @@
// Google Meet plugin module implements drive behavior.
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
+import { readProviderTextResponse } from "openclaw/plugin-sdk/provider-http";
import { googleApiError } from "./google-api-errors.js";
const GOOGLE_DRIVE_API_BASE_URL = "https://www.googleapis.com/drive/v3";
@@ -64,7 +65,7 @@ export async function exportGoogleDriveDocumentText(params: {
scopes: [GOOGLE_DRIVE_MEET_SCOPE],
});
}
- return await response.text();
+ return await readProviderTextResponse(response, "Google Drive files.export");
} finally {
await release();
}