diff --git a/extensions/google/transport-stream.test.ts b/extensions/google/transport-stream.test.ts index d295c29f9a72..eb388b55a49e 100644 --- a/extensions/google/transport-stream.test.ts +++ b/extensions/google/transport-stream.test.ts @@ -1108,6 +1108,55 @@ describe("google transport stream", () => { expect(result.content).toEqual([{ type: "text", text: "ok" }]); }); + it("rejects oversized authorized_user ADC token responses", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-large-")); + const credentialsPath = path.join(tempDir, "application_default_credentials.json"); + await writeFile( + credentialsPath, + JSON.stringify({ + type: "authorized_user", + client_id: "client-id", + client_secret: "client-secret", + refresh_token: "large-refresh-token", + }), + "utf8", + ); + vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath); + const tokenFetchMock = vi + .fn() + .mockResolvedValue(new Response("x".repeat(1024 * 1024 + 1), { status: 200 })); + + await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).rejects.toThrow( + "Google OAuth token response exceeds 1048576 bytes", + ); + }); + + it("rejects authorized_user ADC gzip responses that expand past the limit", async () => { + const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-bomb-")); + const credentialsPath = path.join(tempDir, "application_default_credentials.json"); + await writeFile( + credentialsPath, + JSON.stringify({ + type: "authorized_user", + client_id: "client-id", + client_secret: "client-secret", + refresh_token: "bomb-refresh-token", + }), + "utf8", + ); + vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath); + const tokenFetchMock = vi.fn().mockResolvedValue( + new Response(gzipSync("x".repeat(1024 * 1024 + 1)), { + status: 200, + headers: { "content-encoding": "gzip" }, + }), + ); + + await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).rejects.toThrow( + "Google OAuth token response exceeds 1048576 decompressed bytes", + ); + }); + it("refreshes authorized_user ADC from a compressed token response", async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-gzip-")); const credentialsPath = path.join(tempDir, "application_default_credentials.json"); diff --git a/extensions/google/vertex-adc.ts b/extensions/google/vertex-adc.ts index 9bf2e45c7998..2300fa5de53c 100644 --- a/extensions/google/vertex-adc.ts +++ b/extensions/google/vertex-adc.ts @@ -9,6 +9,7 @@ import { resolveExpiresAtMsFromDurationMs, resolveExpiresAtMsFromDurationSeconds, } from "openclaw/plugin-sdk/number-runtime"; +import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; type GoogleAuthorizedUserCredentials = { @@ -46,6 +47,7 @@ const GOOGLE_VERTEX_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platfor const GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS = 60_000; const GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; const GOOGLE_VERTEX_AUTHLIB_TOKEN_CACHE_MS = 5 * 60_000; +const GOOGLE_OAUTH_TOKEN_RESPONSE_MAX_BYTES = 1024 * 1024; let cachedGoogleVertexAuthorizedUserToken: GoogleVertexAuthorizedUserToken | undefined; let cachedGoogleAuthClient: @@ -277,7 +279,10 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: { async function readGoogleOauthTokenResponsePayload( response: Response, ): Promise { - const bytes = Buffer.from(await response.arrayBuffer()); + const bytes = await readResponseWithLimit(response, GOOGLE_OAUTH_TOKEN_RESPONSE_MAX_BYTES, { + onOverflow: ({ maxBytes }) => + new Error(`Google OAuth token response exceeds ${maxBytes} bytes`), + }); const text = decodeGoogleOauthTokenResponseBody(bytes, response.headers.get("content-encoding")); if (!text.trim()) { return undefined; @@ -292,8 +297,21 @@ async function readGoogleOauthTokenResponsePayload( function decodeGoogleOauthTokenResponseBody(bytes: Buffer, contentEncoding: string | null): string { if (shouldGunzipGoogleOauthTokenResponse(bytes, contentEncoding)) { try { - return gunzipSync(bytes).toString("utf8"); - } catch { + return gunzipSync(bytes, { maxOutputLength: GOOGLE_OAUTH_TOKEN_RESPONSE_MAX_BYTES }).toString( + "utf8", + ); + } catch (error) { + if ( + typeof error === "object" && + error !== null && + "code" in error && + error.code === "ERR_BUFFER_TOO_LARGE" + ) { + throw new Error( + `Google OAuth token response exceeds ${GOOGLE_OAUTH_TOKEN_RESPONSE_MAX_BYTES} decompressed bytes`, + { cause: error }, + ); + } return bytes.toString("utf8"); } }