fix(googlechat): reject invalid UTF-8 in API JSON responses (#120239)

… regression

Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
sunlit-deng
2026-08-09 09:54:17 +08:00
committed by GitHub
parent 62b418d551
commit 6287c88c3b
3 changed files with 68 additions and 8 deletions
@@ -64,6 +64,7 @@ vi.mock("./auth.js", () => ({
}));
let deleteGoogleChatMessage: typeof import("./api.js").deleteGoogleChatMessage;
let sendGoogleChatMessage: typeof import("./api.js").sendGoogleChatMessage;
const account = {
accountId: "default",
@@ -117,7 +118,7 @@ async function withinDeadline<T>(promise: Promise<T>, timeoutMs = 2_000): Promis
describe("deleteGoogleChatMessage real guarded transport", () => {
beforeAll(async () => {
({ deleteGoogleChatMessage } = await import("./api.js"));
({ deleteGoogleChatMessage, sendGoogleChatMessage } = await import("./api.js"));
});
beforeEach(() => {
@@ -133,6 +134,32 @@ describe("deleteGoogleChatMessage real guarded transport", () => {
vi.restoreAllMocks();
});
it("rejects malformed UTF-8 JSON through the real guarded transport", async () => {
const body = new Uint8Array([
...new TextEncoder().encode('{"name":"spaces/'),
0xff,
...new TextEncoder().encode('AAA"}'),
]);
const server = createServer((_request, response) => {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(body);
});
loopback.baseUrl = await listen(server);
try {
const outcome = await withinDeadline(
sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }).then(
() => undefined,
(error: unknown) => error,
),
);
expect(outcome).toBeInstanceOf(Error);
expect((outcome as Error).message).toMatch(/malformed JSON response/);
} finally {
await closeServer(server);
}
});
it("cancels a streaming authenticated DELETE before releasing its real dispatcher", async () => {
let socketClosed = false;
let receivedAuthorization: string | undefined;
+3 -7
View File
@@ -6,6 +6,7 @@ import {
parseMediaContentLength,
readResponseTextSnippet,
} from "openclaw/plugin-sdk/media-runtime";
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
@@ -33,17 +34,12 @@ function resolveGoogleChatMediaTimeoutMs(maxBytes?: number): number {
}
async function readGoogleChatJsonResponse<T>(response: Response, label: string): Promise<T> {
const bytes = await readResponseWithLimit(response, GOOGLECHAT_JSON_RESPONSE_MAX_BYTES, {
return readProviderJsonResponse<T>(response, label, {
maxBytes: GOOGLECHAT_JSON_RESPONSE_MAX_BYTES,
chunkTimeoutMs: GOOGLECHAT_RESPONSE_READ_IDLE_TIMEOUT_MS,
onIdleTimeout: ({ chunkTimeoutMs }) =>
new Error(`${label}: response body stalled after ${chunkTimeoutMs}ms`),
onOverflow: ({ maxBytes }) => new Error(`${label}: JSON response exceeds ${maxBytes} bytes`),
});
try {
return JSON.parse(new TextDecoder().decode(bytes)) as T;
} catch (cause) {
throw new Error(`${label}: malformed JSON response`, { cause });
}
}
async function readGoogleChatErrorResponse(response: Response, label: string): Promise<string> {
+37
View File
@@ -287,6 +287,43 @@ describe("googlechat group policy", () => {
});
});
describe("googlechat API JSON response decoding", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("rejects invalid UTF-8 in API JSON responses instead of corrupting identifiers", async () => {
const raw = Buffer.concat([
Buffer.from('{"name":"spaces/'),
Buffer.from([0xff]),
Buffer.from('AAA"}'),
]);
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(new Response(new Uint8Array(raw), { status: 200 })),
);
await expect(
sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }),
).rejects.toThrow(/malformed JSON response/);
});
it("keeps valid UTF-8 API JSON responses unchanged (negative control)", async () => {
vi.stubGlobal(
"fetch",
vi
.fn()
.mockResolvedValue(
new Response(new Uint8Array(Buffer.from('{"name":"spaces/AAA"}')), { status: 200 }),
),
);
await expect(
sendGoogleChatMessage({ account, space: "spaces/AAA", text: "hello" }),
).resolves.toEqual({ messageName: "spaces/AAA", threadName: undefined });
});
});
describe("downloadGoogleChatMedia", () => {
afterEach(() => {
unregisterGoogleChatManualApprovalFollowupSuppression("12345678-1234-1234-1234-123456789012");