mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
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:
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user