From f5160ca6becaeeb6a4dfd892fffd2130a696f766 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Feb 2026 15:18:57 +0100 Subject: [PATCH] test: add browser evaluate gate trust-boundary regression --- ...te-disabled-does-not-block-storage.test.ts | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/browser/server.evaluate-disabled-does-not-block-storage.test.ts diff --git a/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts b/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts new file mode 100644 index 0000000000..b24438f278 --- /dev/null +++ b/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts @@ -0,0 +1,143 @@ +import { createServer, type AddressInfo } from "node:net"; +import { fetch as realFetch } from "undici"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +let testPort = 0; +let prevGatewayPort: string | undefined; + +const pwMocks = vi.hoisted(() => ({ + cookiesGetViaPlaywright: vi.fn(async () => ({ + cookies: [{ name: "session", value: "abc123" }], + })), + storageGetViaPlaywright: vi.fn(async () => ({ values: { token: "value" } })), + evaluateViaPlaywright: vi.fn(async () => "ok"), +})); + +const routeCtxMocks = vi.hoisted(() => { + const profileCtx = { + profile: { cdpUrl: "http://127.0.0.1:9222" }, + ensureTabAvailable: vi.fn(async () => ({ + targetId: "tab-1", + url: "https://example.com", + })), + stopRunningBrowser: vi.fn(async () => {}), + }; + + return { + profileCtx, + createBrowserRouteContext: vi.fn(() => ({ + state: () => ({ resolved: { evaluateEnabled: false } }), + forProfile: vi.fn(() => profileCtx), + mapTabError: vi.fn(() => null), + })), + }; +}); + +vi.mock("../config/config.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + loadConfig: () => ({ + browser: { + enabled: true, + evaluateEnabled: false, + defaultProfile: "openclaw", + profiles: { + openclaw: { cdpPort: testPort + 1, color: "#FF4500" }, + }, + }, + }), + writeConfigFile: vi.fn(async () => {}), + }; +}); + +vi.mock("./pw-ai-module.js", () => ({ + getPwAiModule: vi.fn(async () => pwMocks), +})); + +vi.mock("./server-context.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + createBrowserRouteContext: routeCtxMocks.createBrowserRouteContext, + }; +}); + +async function getFreePort(): Promise { + const probe = createServer(); + await new Promise((resolve, reject) => { + probe.once("error", reject); + probe.listen(0, "127.0.0.1", () => resolve()); + }); + const addr = probe.address() as AddressInfo; + await new Promise((resolve) => probe.close(() => resolve())); + return addr.port; +} + +describe("browser control evaluate gating", () => { + beforeEach(async () => { + testPort = await getFreePort(); + prevGatewayPort = process.env.OPENCLAW_GATEWAY_PORT; + process.env.OPENCLAW_GATEWAY_PORT = String(testPort - 2); + + pwMocks.cookiesGetViaPlaywright.mockClear(); + pwMocks.storageGetViaPlaywright.mockClear(); + pwMocks.evaluateViaPlaywright.mockClear(); + routeCtxMocks.profileCtx.ensureTabAvailable.mockClear(); + routeCtxMocks.profileCtx.stopRunningBrowser.mockClear(); + }); + + afterEach(async () => { + vi.restoreAllMocks(); + if (prevGatewayPort === undefined) { + delete process.env.OPENCLAW_GATEWAY_PORT; + } else { + process.env.OPENCLAW_GATEWAY_PORT = prevGatewayPort; + } + + const { stopBrowserControlServer } = await import("./server.js"); + await stopBrowserControlServer(); + }); + + it("blocks act:evaluate but still allows cookies/storage reads", async () => { + const { startBrowserControlServerFromConfig } = await import("./server.js"); + await startBrowserControlServerFromConfig(); + + const base = `http://127.0.0.1:${testPort}`; + + const evalRes = (await realFetch(`${base}/act`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ kind: "evaluate", fn: "() => 1" }), + }).then((r) => r.json())) as { error?: string }; + + expect(evalRes.error).toContain("browser.evaluateEnabled=false"); + expect(pwMocks.evaluateViaPlaywright).not.toHaveBeenCalled(); + + const cookiesRes = (await realFetch(`${base}/cookies`).then((r) => r.json())) as { + ok: boolean; + cookies?: Array<{ name: string }>; + }; + expect(cookiesRes.ok).toBe(true); + expect(cookiesRes.cookies?.[0]?.name).toBe("session"); + expect(pwMocks.cookiesGetViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: "http://127.0.0.1:9222", + targetId: "tab-1", + }); + + const storageRes = (await realFetch(`${base}/storage/local?key=token`).then((r) => + r.json(), + )) as { + ok: boolean; + values?: Record; + }; + expect(storageRes.ok).toBe(true); + expect(storageRes.values).toEqual({ token: "value" }); + expect(pwMocks.storageGetViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: "http://127.0.0.1:9222", + targetId: "tab-1", + kind: "local", + key: "token", + }); + }); +});