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 <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-03-07 18:22:50 -08:00
committed by Peter Steinberger
parent 436ae8a07c
commit e883d0b556
3 changed files with 10 additions and 3 deletions

View File

@@ -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);

View File

@@ -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`,

View File

@@ -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(),