VYPR
High severityNVD Advisory· Published Mar 5, 2026· Updated Mar 9, 2026

OpenClaw 2026.1.29-beta.1 < 2026.2.14 - Authentication Bypass in Sandbox Browser Bridge Server

CVE-2026-28468

Description

OpenClaw versions 2026.1.29-beta.1 prior to 2026.2.14 contain a vulnerability in the sandbox browser bridge server in which it accepts requests without requiring gateway authentication, allowing local attackers to access browser control endpoints. A local attacker can enumerate tabs, retrieve WebSocket URLs, execute JavaScript, and exfiltrate cookies and session data from authenticated browser contexts.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
>= 2026.1.29-beta.1, < 2026.2.142026.2.14

Affected products

1

Patches

3
cd84885a4ac7

test(browser): cover bridge auth registry fallback

https://github.com/openclaw/openclawPeter SteinbergerFeb 14, 2026via ghsa
1 file changed · +53 0
  • src/browser/client-fetch.bridge-auth-registry.test.ts+53 0 added
    @@ -0,0 +1,53 @@
    +import type { AddressInfo } from "node:net";
    +import { createServer } from "node:http";
    +import { afterEach, describe, expect, it, vi } from "vitest";
    +import { deleteBridgeAuthForPort, setBridgeAuthForPort } from "./bridge-auth-registry.js";
    +
    +describe("fetchBrowserJson loopback auth (bridge auth registry)", () => {
    +  afterEach(() => {
    +    vi.resetModules();
    +    vi.clearAllMocks();
    +  });
    +
    +  it("falls back to per-port bridge auth when config auth is not available", async () => {
    +    vi.doMock("../config/config.js", async (importOriginal) => {
    +      const original = await importOriginal<typeof import("../config/config.js")>();
    +      return {
    +        ...original,
    +        loadConfig: () => ({}),
    +      };
    +    });
    +
    +    const server = createServer((req, res) => {
    +      const auth = String(req.headers.authorization ?? "").trim();
    +      if (auth !== "Bearer registry-token") {
    +        res.statusCode = 401;
    +        res.setHeader("Content-Type", "text/plain; charset=utf-8");
    +        res.end("Unauthorized");
    +        return;
    +      }
    +      res.statusCode = 200;
    +      res.setHeader("Content-Type", "application/json; charset=utf-8");
    +      res.end(JSON.stringify({ ok: true }));
    +    });
    +
    +    await new Promise<void>((resolve, reject) => {
    +      server.once("error", reject);
    +      server.listen(0, "127.0.0.1", () => resolve());
    +    });
    +
    +    const port = (server.address() as AddressInfo).port;
    +    setBridgeAuthForPort(port, { token: "registry-token" });
    +
    +    try {
    +      const { fetchBrowserJson } = await import("./client-fetch.js");
    +      const result = await fetchBrowserJson<{ ok: boolean }>(`http://127.0.0.1:${port}/`, {
    +        timeoutMs: 2000,
    +      });
    +      expect(result.ok).toBe(true);
    +    } finally {
    +      deleteBridgeAuthForPort(port);
    +      await new Promise<void>((resolve) => server.close(() => resolve()));
    +    }
    +  });
    +});
    
6dd6bce997c4

fix(security): enforce sandbox bridge auth

https://github.com/openclaw/openclawPeter SteinbergerFeb 14, 2026via ghsa
8 files changed · +108 5
  • CHANGELOG.md+1 0 modified
    @@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
     - Security/Link understanding: block loopback/internal host patterns and private/mapped IPv6 addresses in extracted URL handling to close SSRF bypasses in link CLI flows. (#15604) Thanks @AI-Reviewer-QS.
     - Security/Browser: constrain `POST /trace/stop`, `POST /wait/download`, and `POST /download` output paths to OpenClaw temp roots and reject traversal/escape paths.
     - Security/Browser: require auth for the sandbox browser bridge server (protects `/profiles`, `/tabs`, CDP URLs, and other control endpoints). Thanks @jackhax.
    +- Security: bind local helper servers to loopback and fail closed on non-loopback OAuth callback hosts (reduces localhost/LAN attack surface).
     - Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane.
     - Security/WhatsApp: enforce `0o600` on `creds.json` and `creds.json.bak` on save/backup/restore paths to reduce credential file exposure. (#10529) Thanks @abdelsfane.
     - Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow.
    
  • src/agents/sandbox/browser.ts+15 3 modified
    @@ -1,3 +1,4 @@
    +import crypto from "node:crypto";
     import type { SandboxBrowserContext, SandboxConfig } from "./types.js";
     import { startBrowserBridgeServer, stopBrowserBridgeServer } from "../../browser/bridge-server.js";
     import { type ResolvedBrowserConfig, resolveProfile } from "../../browser/config.js";
    @@ -149,13 +150,24 @@ export async function ensureSandboxBrowser(params: {
           ? await readDockerPort(containerName, params.cfg.browser.noVncPort)
           : null;
     
    -  const desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined;
    -  const desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined;
    -
       const existing = BROWSER_BRIDGES.get(params.scopeKey);
       const existingProfile = existing
         ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME)
         : null;
    +
    +  let desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined;
    +  let desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined;
    +  if (!desiredAuthToken && !desiredAuthPassword) {
    +    // Always require auth for the sandbox bridge server, even if gateway auth
    +    // mode doesn't produce a shared secret (e.g. trusted-proxy).
    +    // Keep it stable across calls by reusing the existing bridge auth.
    +    desiredAuthToken = existing?.authToken;
    +    desiredAuthPassword = existing?.authPassword;
    +    if (!desiredAuthToken && !desiredAuthPassword) {
    +      desiredAuthToken = crypto.randomBytes(24).toString("hex");
    +    }
    +  }
    +
       const shouldReuse =
         existing && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp;
       const authMatches =
    
  • src/browser/bridge-auth-registry.ts+34 0 added
    @@ -0,0 +1,34 @@
    +type BridgeAuth = {
    +  token?: string;
    +  password?: string;
    +};
    +
    +// In-process registry for loopback-only bridge servers that require auth, but
    +// are addressed via dynamic ephemeral ports (e.g. sandbox browser bridge).
    +const authByPort = new Map<number, BridgeAuth>();
    +
    +export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
    +  if (!Number.isFinite(port) || port <= 0) {
    +    return;
    +  }
    +  const token = typeof auth.token === "string" ? auth.token.trim() : "";
    +  const password = typeof auth.password === "string" ? auth.password.trim() : "";
    +  authByPort.set(port, {
    +    token: token || undefined,
    +    password: password || undefined,
    +  });
    +}
    +
    +export function getBridgeAuthForPort(port: number): BridgeAuth | undefined {
    +  if (!Number.isFinite(port) || port <= 0) {
    +    return undefined;
    +  }
    +  return authByPort.get(port);
    +}
    +
    +export function deleteBridgeAuthForPort(port: number): void {
    +  if (!Number.isFinite(port) || port <= 0) {
    +    return;
    +  }
    +  authByPort.delete(port);
    +}
    
  • src/browser/bridge-server.auth.test.ts+8 0 modified
    @@ -73,4 +73,12 @@ describe("startBrowserBridgeServer auth", () => {
         });
         expect(authed.status).toBe(200);
       });
    +
    +  it("requires auth params", async () => {
    +    await expect(
    +      startBrowserBridgeServer({
    +        resolved: buildResolvedConfig(),
    +      }),
    +    ).rejects.toThrow(/requires auth/i);
    +  });
     });
    
  • src/browser/bridge-server.ts+18 0 modified
    @@ -4,7 +4,9 @@ import type { AddressInfo } from "node:net";
     import express from "express";
     import type { ResolvedBrowserConfig } from "./config.js";
     import type { BrowserRouteRegistrar } from "./routes/types.js";
    +import { isLoopbackHost } from "../gateway/net.js";
     import { safeEqualSecret } from "../security/secret-equal.js";
    +import { deleteBridgeAuthForPort, setBridgeAuthForPort } from "./bridge-auth-registry.js";
     import { registerBrowserRoutes } from "./routes/index.js";
     import {
       type BrowserServerState,
    @@ -89,6 +91,9 @@ export async function startBrowserBridgeServer(params: {
       onEnsureAttachTarget?: (profile: ProfileContext["profile"]) => Promise<void>;
     }): Promise<BrowserBridge> {
       const host = params.host ?? "127.0.0.1";
    +  if (!isLoopbackHost(host)) {
    +    throw new Error(`bridge server must bind to loopback host (got ${host})`);
    +  }
       const port = params.port ?? 0;
     
       const app = express();
    @@ -109,6 +114,9 @@ export async function startBrowserBridgeServer(params: {
     
       const authToken = params.authToken?.trim() || undefined;
       const authPassword = params.authPassword?.trim() || undefined;
    +  if (!authToken && !authPassword) {
    +    throw new Error("bridge server requires auth (authToken/authPassword missing)");
    +  }
       if (authToken || authPassword) {
         app.use((req, res, next) => {
           if (isAuthorizedBrowserRequest(req, { token: authToken, password: authPassword })) {
    @@ -142,11 +150,21 @@ export async function startBrowserBridgeServer(params: {
       state.port = resolvedPort;
       state.resolved.controlPort = resolvedPort;
     
    +  setBridgeAuthForPort(resolvedPort, { token: authToken, password: authPassword });
    +
       const baseUrl = `http://${host}:${resolvedPort}`;
       return { server, port: resolvedPort, baseUrl, state };
     }
     
     export async function stopBrowserBridgeServer(server: Server): Promise<void> {
    +  try {
    +    const address = server.address() as AddressInfo | null;
    +    if (address?.port) {
    +      deleteBridgeAuthForPort(address.port);
    +    }
    +  } catch {
    +    // ignore
    +  }
       await new Promise<void>((resolve) => {
         server.close(() => resolve());
       });
    
  • src/browser/client-fetch.ts+25 1 modified
    @@ -1,5 +1,6 @@
     import { formatCliCommand } from "../cli/command-format.js";
     import { loadConfig } from "../config/config.js";
    +import { getBridgeAuthForPort } from "./bridge-auth-registry.js";
     import { resolveBrowserControlAuth } from "./control-auth.js";
     import {
       createBrowserControlContext,
    @@ -37,13 +38,36 @@ function withLoopbackBrowserAuth(
         const auth = resolveBrowserControlAuth(cfg);
         if (auth.token) {
           headers.set("Authorization", `Bearer ${auth.token}`);
    -    } else if (auth.password) {
    +      return { ...init, headers };
    +    }
    +    if (auth.password) {
           headers.set("x-openclaw-password", auth.password);
    +      return { ...init, headers };
         }
       } catch {
         // ignore config/auth lookup failures and continue without auth headers
       }
     
    +  // Sandbox bridge servers can run with per-process ephemeral auth on dynamic ports.
    +  // Fall back to the in-memory registry if config auth is not available.
    +  try {
    +    const parsed = new URL(url);
    +    const port =
    +      parsed.port && Number.parseInt(parsed.port, 10) > 0
    +        ? Number.parseInt(parsed.port, 10)
    +        : parsed.protocol === "https:"
    +          ? 443
    +          : 80;
    +    const bridgeAuth = getBridgeAuthForPort(port);
    +    if (bridgeAuth?.token) {
    +      headers.set("Authorization", `Bearer ${bridgeAuth.token}`);
    +    } else if (bridgeAuth?.password) {
    +      headers.set("x-openclaw-password", bridgeAuth.password);
    +    }
    +  } catch {
    +    // ignore
    +  }
    +
       return { ...init, headers };
     }
     
    
  • src/commands/chutes-oauth.ts+6 0 modified
    @@ -8,6 +8,7 @@ import {
       generateChutesPkce,
       parseOAuthCallbackInput,
     } from "../agents/chutes-oauth.js";
    +import { isLoopbackHost } from "../gateway/net.js";
     
     type OAuthPrompt = {
       message: string;
    @@ -44,6 +45,11 @@ async function waitForLocalCallback(params: {
         throw new Error(`Chutes OAuth redirect URI must be http:// (got ${params.redirectUri})`);
       }
       const hostname = redirectUrl.hostname || "127.0.0.1";
    +  if (!isLoopbackHost(hostname)) {
    +    throw new Error(
    +      `Chutes OAuth redirect hostname must be loopback (got ${hostname}). Use http://127.0.0.1:<port>/...`,
    +    );
    +  }
       const port = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 80;
       const expectedPath = redirectUrl.pathname || "/";
     
    
  • src/media/server.ts+1 1 modified
    @@ -96,7 +96,7 @@ export async function startMediaServer(
       const app = express();
       attachMediaRoutes(app, ttlMs, runtime);
       return await new Promise((resolve, reject) => {
    -    const server = app.listen(port);
    +    const server = app.listen(port, "127.0.0.1");
         server.once("listening", () => resolve(server));
         server.once("error", (err) => {
           runtime.error(danger(`Media server failed: ${String(err)}`));
    
4711a943e30b

fix(browser): authenticate sandbox browser bridge server

https://github.com/openclaw/openclawPeter SteinbergerFeb 14, 2026via ghsa
6 files changed · +191 7
  • CHANGELOG.md+1 0 modified
    @@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
     - Security/Gateway: breaking default-behavior change - canvas IP-based auth fallback now only accepts machine-scoped addresses (RFC1918, link-local, ULA IPv6, CGNAT); public-source IP matches now require bearer token auth. (#14661) Thanks @sumleo.
     - Security/Link understanding: block loopback/internal host patterns and private/mapped IPv6 addresses in extracted URL handling to close SSRF bypasses in link CLI flows. (#15604) Thanks @AI-Reviewer-QS.
     - Security/Browser: constrain `POST /trace/stop`, `POST /wait/download`, and `POST /download` output paths to OpenClaw temp roots and reject traversal/escape paths.
    +- Security/Browser: require auth for the sandbox browser bridge server (protects `/profiles`, `/tabs`, CDP URLs, and other control endpoints). Thanks @jackhax.
     - Security/Canvas: serve A2UI assets via the shared safe-open path (`openFileWithinRoot`) to close traversal/TOCTOU gaps, with traversal and symlink regression coverage. (#10525) Thanks @abdelsfane.
     - Security/WhatsApp: enforce `0o600` on `creds.json` and `creds.json.bak` on save/backup/restore paths to reduce credential file exposure. (#10529) Thanks @abdelsfane.
     - Security/Gateway: sanitize and truncate untrusted WebSocket header values in pre-handshake close logs to reduce log-poisoning risk. Thanks @thewilloftheshadow.
    
  • src/agents/sandbox/browser-bridges.ts+9 1 modified
    @@ -1,3 +1,11 @@
     import type { BrowserBridge } from "../../browser/bridge-server.js";
     
    -export const BROWSER_BRIDGES = new Map<string, { bridge: BrowserBridge; containerName: string }>();
    +export const BROWSER_BRIDGES = new Map<
    +  string,
    +  {
    +    bridge: BrowserBridge;
    +    containerName: string;
    +    authToken?: string;
    +    authPassword?: string;
    +  }
    +>();
    
  • src/agents/sandbox/browser.ts+17 2 modified
    @@ -90,6 +90,7 @@ export async function ensureSandboxBrowser(params: {
       agentWorkspaceDir: string;
       cfg: SandboxConfig;
       evaluateEnabled?: boolean;
    +  bridgeAuth?: { token?: string; password?: string };
     }): Promise<SandboxBrowserContext | null> {
       if (!params.cfg.browser.enabled) {
         return null;
    @@ -148,19 +149,29 @@ export async function ensureSandboxBrowser(params: {
           ? await readDockerPort(containerName, params.cfg.browser.noVncPort)
           : null;
     
    +  const desiredAuthToken = params.bridgeAuth?.token?.trim() || undefined;
    +  const desiredAuthPassword = params.bridgeAuth?.password?.trim() || undefined;
    +
       const existing = BROWSER_BRIDGES.get(params.scopeKey);
       const existingProfile = existing
         ? resolveProfile(existing.bridge.state.resolved, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME)
         : null;
       const shouldReuse =
         existing && existing.containerName === containerName && existingProfile?.cdpPort === mappedCdp;
    +  const authMatches =
    +    !existing ||
    +    (existing.authToken === desiredAuthToken && existing.authPassword === desiredAuthPassword);
       if (existing && !shouldReuse) {
         await stopBrowserBridgeServer(existing.bridge.server).catch(() => undefined);
         BROWSER_BRIDGES.delete(params.scopeKey);
       }
    +  if (existing && shouldReuse && !authMatches) {
    +    await stopBrowserBridgeServer(existing.bridge.server).catch(() => undefined);
    +    BROWSER_BRIDGES.delete(params.scopeKey);
    +  }
     
       const bridge = (() => {
    -    if (shouldReuse && existing) {
    +    if (shouldReuse && authMatches && existing) {
           return existing.bridge;
         }
         return null;
    @@ -196,15 +207,19 @@ export async function ensureSandboxBrowser(params: {
             headless: params.cfg.browser.headless,
             evaluateEnabled: params.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED,
           }),
    +      authToken: desiredAuthToken,
    +      authPassword: desiredAuthPassword,
           onEnsureAttachTarget,
         });
       };
     
       const resolvedBridge = await ensureBridge();
    -  if (!shouldReuse) {
    +  if (!shouldReuse || !authMatches) {
         BROWSER_BRIDGES.set(params.scopeKey, {
           bridge: resolvedBridge,
           containerName,
    +      authToken: desiredAuthToken,
    +      authPassword: desiredAuthPassword,
         });
       }
     
    
  • src/agents/sandbox/context.ts+20 0 modified
    @@ -2,6 +2,8 @@ import fs from "node:fs/promises";
     import type { OpenClawConfig } from "../../config/config.js";
     import type { SandboxContext, SandboxWorkspaceInfo } from "./types.js";
     import { DEFAULT_BROWSER_EVALUATE_ENABLED } from "../../browser/constants.js";
    +import { ensureBrowserControlAuth, resolveBrowserControlAuth } from "../../browser/control-auth.js";
    +import { loadConfig } from "../../config/config.js";
     import { defaultRuntime } from "../../runtime.js";
     import { resolveUserPath } from "../../utils.js";
     import { syncSkillsToWorkspace } from "../skills.js";
    @@ -76,12 +78,30 @@ export async function resolveSandboxContext(params: {
     
       const evaluateEnabled =
         params.config?.browser?.evaluateEnabled ?? DEFAULT_BROWSER_EVALUATE_ENABLED;
    +
    +  const bridgeAuth = cfg.browser.enabled
    +    ? await (async () => {
    +        // Sandbox browser bridge server runs on a loopback TCP port; always wire up
    +        // the same auth that loopback browser clients will send (token/password).
    +        const cfgForAuth = params.config ?? loadConfig();
    +        let browserAuth = resolveBrowserControlAuth(cfgForAuth);
    +        try {
    +          const ensured = await ensureBrowserControlAuth({ cfg: cfgForAuth });
    +          browserAuth = ensured.auth;
    +        } catch (error) {
    +          const message = error instanceof Error ? error.message : JSON.stringify(error);
    +          defaultRuntime.error?.(`Sandbox browser auth ensure failed: ${message}`);
    +        }
    +        return browserAuth;
    +      })()
    +    : undefined;
       const browser = await ensureSandboxBrowser({
         scopeKey,
         workspaceDir,
         agentWorkspaceDir,
         cfg,
         evaluateEnabled,
    +    bridgeAuth,
       });
     
       const sandboxContext: SandboxContext = {
    
  • src/browser/bridge-server.auth.test.ts+76 0 added
    @@ -0,0 +1,76 @@
    +import { afterEach, describe, expect, it } from "vitest";
    +import { startBrowserBridgeServer, stopBrowserBridgeServer } from "./bridge-server.js";
    +import {
    +  DEFAULT_OPENCLAW_BROWSER_COLOR,
    +  DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
    +} from "./constants.js";
    +
    +function buildResolvedConfig() {
    +  return {
    +    enabled: true,
    +    evaluateEnabled: false,
    +    controlPort: 0,
    +    cdpProtocol: "http",
    +    cdpHost: "127.0.0.1",
    +    cdpIsLoopback: true,
    +    remoteCdpTimeoutMs: 1500,
    +    remoteCdpHandshakeTimeoutMs: 3000,
    +    color: DEFAULT_OPENCLAW_BROWSER_COLOR,
    +    executablePath: undefined,
    +    headless: true,
    +    noSandbox: false,
    +    attachOnly: true,
    +    defaultProfile: DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
    +    profiles: {
    +      [DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME]: {
    +        cdpPort: 1,
    +        color: DEFAULT_OPENCLAW_BROWSER_COLOR,
    +      },
    +    },
    +  } as const;
    +}
    +
    +describe("startBrowserBridgeServer auth", () => {
    +  const servers: Array<{ stop: () => Promise<void> }> = [];
    +
    +  afterEach(async () => {
    +    while (servers.length) {
    +      const s = servers.pop();
    +      if (s) {
    +        await s.stop();
    +      }
    +    }
    +  });
    +
    +  it("rejects unauthenticated requests when authToken is set", async () => {
    +    const bridge = await startBrowserBridgeServer({
    +      resolved: buildResolvedConfig(),
    +      authToken: "secret-token",
    +    });
    +    servers.push({ stop: () => stopBrowserBridgeServer(bridge.server) });
    +
    +    const unauth = await fetch(`${bridge.baseUrl}/`);
    +    expect(unauth.status).toBe(401);
    +
    +    const authed = await fetch(`${bridge.baseUrl}/`, {
    +      headers: { Authorization: "Bearer secret-token" },
    +    });
    +    expect(authed.status).toBe(200);
    +  });
    +
    +  it("accepts x-openclaw-password when authPassword is set", async () => {
    +    const bridge = await startBrowserBridgeServer({
    +      resolved: buildResolvedConfig(),
    +      authPassword: "secret-password",
    +    });
    +    servers.push({ stop: () => stopBrowserBridgeServer(bridge.server) });
    +
    +    const unauth = await fetch(`${bridge.baseUrl}/`);
    +    expect(unauth.status).toBe(401);
    +
    +    const authed = await fetch(`${bridge.baseUrl}/`, {
    +      headers: { "x-openclaw-password": "secret-password" },
    +    });
    +    expect(authed.status).toBe(200);
    +  });
    +});
    
  • src/browser/bridge-server.ts+68 4 modified
    @@ -1,15 +1,78 @@
     import type { Server } from "node:http";
    +import type { IncomingMessage } from "node:http";
     import type { AddressInfo } from "node:net";
     import express from "express";
     import type { ResolvedBrowserConfig } from "./config.js";
     import type { BrowserRouteRegistrar } from "./routes/types.js";
    +import { safeEqualSecret } from "../security/secret-equal.js";
     import { registerBrowserRoutes } from "./routes/index.js";
     import {
       type BrowserServerState,
       createBrowserRouteContext,
       type ProfileContext,
     } from "./server-context.js";
     
    +function firstHeaderValue(value: string | string[] | undefined): string {
    +  return Array.isArray(value) ? (value[0] ?? "") : (value ?? "");
    +}
    +
    +function parseBearerToken(authorization: string): string | undefined {
    +  if (!authorization || !authorization.toLowerCase().startsWith("bearer ")) {
    +    return undefined;
    +  }
    +  const token = authorization.slice(7).trim();
    +  return token || undefined;
    +}
    +
    +function parseBasicPassword(authorization: string): string | undefined {
    +  if (!authorization || !authorization.toLowerCase().startsWith("basic ")) {
    +    return undefined;
    +  }
    +  const encoded = authorization.slice(6).trim();
    +  if (!encoded) {
    +    return undefined;
    +  }
    +  try {
    +    const decoded = Buffer.from(encoded, "base64").toString("utf8");
    +    const sep = decoded.indexOf(":");
    +    if (sep < 0) {
    +      return undefined;
    +    }
    +    const password = decoded.slice(sep + 1).trim();
    +    return password || undefined;
    +  } catch {
    +    return undefined;
    +  }
    +}
    +
    +function isAuthorizedBrowserRequest(
    +  req: IncomingMessage,
    +  auth: { token?: string; password?: string },
    +): boolean {
    +  const authorization = firstHeaderValue(req.headers.authorization).trim();
    +
    +  if (auth.token) {
    +    const bearer = parseBearerToken(authorization);
    +    if (bearer && safeEqualSecret(bearer, auth.token)) {
    +      return true;
    +    }
    +  }
    +
    +  if (auth.password) {
    +    const passwordHeader = firstHeaderValue(req.headers["x-openclaw-password"]).trim();
    +    if (passwordHeader && safeEqualSecret(passwordHeader, auth.password)) {
    +      return true;
    +    }
    +
    +    const basicPassword = parseBasicPassword(authorization);
    +    if (basicPassword && safeEqualSecret(basicPassword, auth.password)) {
    +      return true;
    +    }
    +  }
    +
    +  return false;
    +}
    +
     export type BrowserBridge = {
       server: Server;
       port: number;
    @@ -22,6 +85,7 @@ export async function startBrowserBridgeServer(params: {
       host?: string;
       port?: number;
       authToken?: string;
    +  authPassword?: string;
       onEnsureAttachTarget?: (profile: ProfileContext["profile"]) => Promise<void>;
     }): Promise<BrowserBridge> {
       const host = params.host ?? "127.0.0.1";
    @@ -43,11 +107,11 @@ export async function startBrowserBridgeServer(params: {
       });
       app.use(express.json({ limit: "1mb" }));
     
    -  const authToken = params.authToken?.trim();
    -  if (authToken) {
    +  const authToken = params.authToken?.trim() || undefined;
    +  const authPassword = params.authPassword?.trim() || undefined;
    +  if (authToken || authPassword) {
         app.use((req, res, next) => {
    -      const auth = String(req.headers.authorization ?? "").trim();
    -      if (auth === `Bearer ${authToken}`) {
    +      if (isAuthorizedBrowserRequest(req, { token: authToken, password: authPassword })) {
             return next();
           }
           res.status(401).send("Unauthorized");
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.