From e883d0b556b069d216e76fedbb17d4b59babd35d Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 18:22:50 -0800 Subject: [PATCH] fix(browser): add IP validation, fix upgrade handler for non-loopback bind - Zod schema: validate relayBindHost with ipv4/ipv6 instead of bare string - Upgrade handler: allow non-loopback connections when bindHost is explicitly non-loopback (e.g. 0.0.0.0 for WSL2), keeping loopback-only default - Test: verify actual bind address via relay.bindHost instead of just checking reachability on 127.0.0.1 which passes regardless - Expose bindHost on ChromeExtensionRelayServer type for inspection Co-Authored-By: Claude Opus 4.6 --- src/browser/extension-relay.test.ts | 4 +++- src/browser/extension-relay.ts | 7 ++++++- src/config/zod-schema.ts | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/browser/extension-relay.test.ts b/src/browser/extension-relay.test.ts index 7208d0dd10b..b95dea6c9f2 100644 --- a/src/browser/extension-relay.test.ts +++ b/src/browser/extension-relay.test.ts @@ -1179,8 +1179,9 @@ describe("chrome extension relay server", () => { bindHost: "0.0.0.0", }); expect(relay.port).toBe(port); + // Verify the server actually bound to 0.0.0.0, not the cdpUrl host. + expect(relay.bindHost).toBe("0.0.0.0"); - // Relay should be reachable on loopback (0.0.0.0 accepts all interfaces). const res = await fetch(`http://127.0.0.1:${port}/`); expect(res.status).toBe(200); }, @@ -1194,6 +1195,7 @@ describe("chrome extension relay server", () => { cdpUrl = `http://127.0.0.1:${port}`; const relay = await ensureChromeExtensionRelayServer({ cdpUrl }); expect(relay.host).toBe("127.0.0.1"); + expect(relay.bindHost).toBe("127.0.0.1"); const res = await fetch(`http://127.0.0.1:${port}/`); expect(res.status).toBe(200); diff --git a/src/browser/extension-relay.ts b/src/browser/extension-relay.ts index 2107fff4a32..603ae579c12 100644 --- a/src/browser/extension-relay.ts +++ b/src/browser/extension-relay.ts @@ -113,6 +113,7 @@ function getRelayAuthTokenFromRequest(req: IncomingMessage, url?: URL): string | export type ChromeExtensionRelayServer = { host: string; + bindHost: string; port: number; baseUrl: string; cdpWsUrl: string; @@ -684,7 +685,9 @@ export async function ensureChromeExtensionRelayServer(opts: { const pathname = url.pathname; const remote = req.socket.remoteAddress; - if (!isLoopbackAddress(remote)) { + // When bindHost is explicitly non-loopback (e.g. 0.0.0.0 for WSL2), + // allow non-loopback connections; otherwise enforce loopback-only. + if (!isLoopbackAddress(remote) && isLoopbackHost(bindHost)) { rejectUpgrade(socket, 403, "Forbidden"); return; } @@ -978,6 +981,7 @@ export async function ensureChromeExtensionRelayServer(opts: { ) { const existingRelay: ChromeExtensionRelayServer = { host: info.host, + bindHost, port: info.port, baseUrl: info.baseUrl, cdpWsUrl: `ws://${info.host}:${info.port}/cdp`, @@ -999,6 +1003,7 @@ export async function ensureChromeExtensionRelayServer(opts: { const relay: ChromeExtensionRelayServer = { host, + bindHost, port, baseUrl, cdpWsUrl: `ws://${host}:${port}/cdp`, diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index de4a503cdbf..c35d1191b6f 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -372,7 +372,7 @@ export const OpenClawSchema = z ) .optional(), extraArgs: z.array(z.string()).optional(), - relayBindHost: z.string().optional(), + relayBindHost: z.union([z.string().ipv4(), z.string().ipv6()]).optional(), }) .strict() .optional(),