fix(google): bound Vertex token response bodies

Partial prerequisite backport from aaf5ab910c (#100483).

(cherry picked from commit 8a7a6acb6d)
This commit is contained in:
Dallin Romney
2026-07-09 20:31:28 -07:00
parent 311f664e69
commit 95dcba5345
2 changed files with 70 additions and 3 deletions
@@ -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");
+21 -3
View File
@@ -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<GoogleOauthTokenResponsePayload | undefined> {
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");
}
}