VYPR
Critical severity9.8NVD Advisory· Published May 6, 2026· Updated May 7, 2026

CVE-2026-43575

CVE-2026-43575

Description

OpenClaw versions 2026.2.21 before 2026.4.10 contain an authentication bypass vulnerability in the sandbox noVNC helper route that exposes interactive browser session credentials. Attackers can access the noVNC helper route without bridge authentication to gain unauthorized access to the interactive browser session.

Affected products

2
  • OpenClaw/Openclawreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*range: >=2026.2.21,<2026.4.10

Patches

1
8dfbf3268bd2

fix(browser): gate sandbox noVNC helper auth

https://github.com/openclaw/openclawAgustin RiveraApr 10, 2026via nvd-ref
9 files changed · +41 19
  • CHANGELOG.md+1 0 modified
    @@ -121,6 +121,7 @@ Docs: https://docs.openclaw.ai
     
     - Cron/isolated agent: run scheduled agent turns as non-owner senders so owner-only tools stay unavailable during cron execution. (#63878)
     - Voice Call/realtime: reject oversized realtime WebSocket frames before bridge setup so large pre-start payloads cannot crash the gateway. (#63890) Thanks @mmaps.
    +- Browser/sandbox: gate `/sandbox/novnc` behind bridge auth and stop surfacing sandbox observer URLs in model-visible prompt context. (#63882) Thanks @eleqtrizit.
     
     - Discord/sandbox: include `image` in sandbox media param normalization so Discord event cover images cannot bypass sandbox path rewriting. (#64377) Thanks @mmaps.
     ## 2026.4.9
    
  • extensions/browser/src/browser/bridge-server.auth.test.ts+10 1 modified
    @@ -83,10 +83,12 @@ describe("startBrowserBridgeServer auth", () => {
       });
     
       it("serves noVNC bootstrap html without leaking password in Location header", async () => {
    +    let resolveCalls = 0;
         const bridge = await startBrowserBridgeServer({
           resolved: buildResolvedConfig(),
           authToken: "secret-token",
           resolveSandboxNoVncToken: (token) => {
    +        resolveCalls += 1;
             if (token !== "valid-token") {
               return null;
             }
    @@ -95,8 +97,15 @@ describe("startBrowserBridgeServer auth", () => {
         });
         servers.push({ stop: () => stopBrowserBridgeServer(bridge.server) });
     
    -    const res = await fetch(`${bridge.baseUrl}/sandbox/novnc?token=valid-token`);
    +    const unauth = await fetch(`${bridge.baseUrl}/sandbox/novnc?token=valid-token`);
    +    expect(unauth.status).toBe(401);
    +    expect(resolveCalls).toBe(0);
    +
    +    const res = await fetch(`${bridge.baseUrl}/sandbox/novnc?token=valid-token`, {
    +      headers: { Authorization: "Bearer secret-token" },
    +    });
         expect(res.status).toBe(200);
    +    expect(resolveCalls).toBe(1);
         expect(res.headers.get("location")).toBeNull();
         expect(res.headers.get("cache-control")).toContain("no-store");
         expect(res.headers.get("referrer-policy")).toBe("no-referrer");
    
  • extensions/browser/src/browser/bridge-server.ts+12 7 modified
    @@ -13,6 +13,7 @@ import {
       type ProfileContext,
     } from "./server-context.js";
     import {
    +  hasVerifiedBrowserAuth,
       installBrowserAuthMiddleware,
       installBrowserCommonMiddleware,
     } from "./server-middleware.js";
    @@ -76,8 +77,19 @@ export async function startBrowserBridgeServer(params: {
       const app = express();
       installBrowserCommonMiddleware(app);
     
    +  const authToken = normalizeOptionalString(params.authToken);
    +  const authPassword = normalizeOptionalString(params.authPassword);
    +  if (!authToken && !authPassword) {
    +    throw new Error("bridge server requires auth (authToken/authPassword missing)");
    +  }
    +  installBrowserAuthMiddleware(app, { token: authToken, password: authPassword });
    +
       if (params.resolveSandboxNoVncToken) {
         app.get("/sandbox/novnc", (req, res) => {
    +      if (!hasVerifiedBrowserAuth(req)) {
    +        res.status(401).send("Unauthorized");
    +        return;
    +      }
           res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate");
           res.setHeader("Pragma", "no-cache");
           res.setHeader("Expires", "0");
    @@ -96,13 +108,6 @@ export async function startBrowserBridgeServer(params: {
         });
       }
     
    -  const authToken = normalizeOptionalString(params.authToken);
    -  const authPassword = normalizeOptionalString(params.authPassword);
    -  if (!authToken && !authPassword) {
    -    throw new Error("bridge server requires auth (authToken/authPassword missing)");
    -  }
    -  installBrowserAuthMiddleware(app, { token: authToken, password: authPassword });
    -
       const state: BrowserServerState = {
         server: null as unknown as Server,
         port,
    
  • extensions/browser/src/browser/server-middleware.ts+16 1 modified
    @@ -1,8 +1,22 @@
    -import type { Express } from "express";
    +import type { Express, Request } from "express";
     import express from "express";
     import { browserMutationGuardMiddleware } from "./csrf.js";
     import { isAuthorizedBrowserRequest } from "./http-auth.js";
     
    +export const BROWSER_AUTH_VERIFIED_FLAG = "__openclawBrowserAuthVerified";
    +
    +type BrowserAuthMarkedRequest = Request & {
    +  [BROWSER_AUTH_VERIFIED_FLAG]?: boolean;
    +};
    +
    +export function hasVerifiedBrowserAuth(req: Request): boolean {
    +  return (req as BrowserAuthMarkedRequest)[BROWSER_AUTH_VERIFIED_FLAG] === true;
    +}
    +
    +function markVerifiedBrowserAuth(req: Request) {
    +  (req as BrowserAuthMarkedRequest)[BROWSER_AUTH_VERIFIED_FLAG] = true;
    +}
    +
     export function installBrowserCommonMiddleware(app: Express) {
       app.use((req, res, next) => {
         const ctrl = new AbortController();
    @@ -30,6 +44,7 @@ export function installBrowserAuthMiddleware(
       }
       app.use((req, res, next) => {
         if (isAuthorizedBrowserRequest(req, auth)) {
    +      markVerifiedBrowserAuth(req);
           return next();
         }
         res.status(401).send("Unauthorized");
    
  • src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts+0 1 modified
    @@ -54,7 +54,6 @@ describe("buildEmbeddedSandboxInfo", () => {
           workspaceAccess: "none",
           agentWorkspaceMount: undefined,
           browserBridgeUrl: "http://localhost:9222",
    -      browserNoVncUrl: "http://localhost:6080",
           hostBrowserAllowed: true,
         });
       });
    
  • src/agents/pi-embedded-runner/sandbox-info.ts+0 1 modified
    @@ -17,7 +17,6 @@ export function buildEmbeddedSandboxInfo(
         workspaceAccess: sandbox.workspaceAccess,
         agentWorkspaceMount: sandbox.workspaceAccess === "ro" ? "/agent" : undefined,
         browserBridgeUrl: sandbox.browser?.bridgeUrl,
    -    browserNoVncUrl: sandbox.browser?.noVncUrl,
         hostBrowserAllowed: sandbox.browserAllowHostControl,
         ...(elevatedAllowed
           ? {
    
  • src/agents/pi-embedded-runner/types.ts+0 1 modified
    @@ -99,7 +99,6 @@ export type EmbeddedSandboxInfo = {
       workspaceAccess?: "none" | "ro" | "rw";
       agentWorkspaceMount?: string;
       browserBridgeUrl?: string;
    -  browserNoVncUrl?: string;
       hostBrowserAllowed?: boolean;
       elevated?: {
         allowed: boolean;
    
  • src/agents/sanitize-for-prompt.test.ts+2 4 modified
    @@ -32,7 +32,7 @@ describe("buildAgentSystemPrompt uses sanitized workspace/sandbox strings", () =
         expect(prompt).not.toContain("\u2028");
       });
     
    -  it("sanitizes sandbox workspace/mount/url strings", () => {
    +  it("sanitizes sandbox workspace and mount strings", () => {
         const prompt = buildAgentSystemPrompt({
           workspaceDir: "/tmp/test",
           sandboxInfo: {
    @@ -41,16 +41,14 @@ describe("buildAgentSystemPrompt uses sanitized workspace/sandbox strings", () =
             workspaceDir: "/host\nspace",
             workspaceAccess: "rw",
             agentWorkspaceMount: "/mnt\u2028mount",
    -        browserNoVncUrl: "http://example.test/\nui",
           },
         });
         expect(prompt).toContain("Sandbox container workdir: /workspace");
         expect(prompt).toContain(
           "Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): /hostspace",
         );
         expect(prompt).toContain("(mounted at /mntmount)");
    -    expect(prompt).toContain("Sandbox browser observer (noVNC): http://example.test/ui");
    -    expect(prompt).not.toContain("\nui");
    +    expect(prompt).not.toContain("Sandbox browser observer (noVNC):");
       });
     });
     
    
  • src/agents/system-prompt.ts+0 3 modified
    @@ -647,9 +647,6 @@ export function buildAgentSystemPrompt(params: {
                   }`
                 : "",
               params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
    -          params.sandboxInfo.browserNoVncUrl
    -            ? `Sandbox browser observer (noVNC): ${sanitizeForPromptLiteral(params.sandboxInfo.browserNoVncUrl)}`
    -            : "",
               params.sandboxInfo.hostBrowserAllowed === true
                 ? "Host browser control: allowed."
                 : params.sandboxInfo.hostBrowserAllowed === false
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

3

News mentions

0

No linked articles in our index yet.