VYPR
Medium severity5.0NVD Advisory· Published May 11, 2026· Updated May 13, 2026

CVE-2026-45000

CVE-2026-45000

Description

OpenClaw before 2026.4.20 contains a server-side request forgery vulnerability in browser CDP profile creation that skips strict-mode SSRF policy checks. Attackers can create stored profiles pointing to private-network or metadata endpoints that bypass security policies and are later probed during normal profile status operations.

Affected products

1

Patches

2
1fd049e3074c

fix: scope remote CDP host allowlist (#68207)

https://github.com/openclaw/openclawPeter SteinbergerApr 18, 2026via nvd-ref
7 files changed · +94 85
  • CHANGELOG.md+1 0 modified
    @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
     
     - Agents/channels: route cross-agent subagent spawns through the target agent's bound channel account while preserving peer and workspace/role-scoped bindings, so child sessions no longer inherit the caller's account in shared rooms, workspaces, or multi-account setups. (#67508) Thanks @lukeboyett and @gumadeiras.
     - Telegram/callbacks: treat permanent callback edit errors as completed updates so stale command pagination buttons no longer wedge the update watermark and block newer Telegram updates. (#68588) Thanks @Lucenx9.
    +- Browser/CDP: allow the selected remote CDP profile host for CDP health and control checks without widening browser navigation SSRF policy, so WSL-to-Windows Chrome endpoints no longer appear offline under strict defaults. Fixes #68108. (#68207) Thanks @Mlightsnow.
     
     ## 2026.4.18
     
    
  • extensions/browser/src/browser/cdp-reachability-policy.test.ts+58 0 added
    @@ -0,0 +1,58 @@
    +import { describe, expect, it } from "vitest";
    +import { resolveCdpReachabilityPolicy } from "./cdp-reachability-policy.js";
    +import type { ResolvedBrowserProfile } from "./config.js";
    +import { assertBrowserNavigationAllowed } from "./navigation-guard.js";
    +
    +function createProfile(overrides: Partial<ResolvedBrowserProfile>): ResolvedBrowserProfile {
    +  return {
    +    name: "remote",
    +    cdpPort: 9223,
    +    cdpUrl: "http://172.29.128.1:9223",
    +    cdpHost: "172.29.128.1",
    +    cdpIsLoopback: false,
    +    color: "#123456",
    +    driver: "openclaw",
    +    attachOnly: false,
    +    ...overrides,
    +  };
    +}
    +
    +describe("CDP reachability policy", () => {
    +  it("allows the selected remote profile CDP host without widening browser navigation policy", async () => {
    +    const browserPolicy = {};
    +    const profile = createProfile({});
    +
    +    expect(resolveCdpReachabilityPolicy(profile, browserPolicy)).toEqual({
    +      allowedHostnames: ["172.29.128.1"],
    +    });
    +    expect(browserPolicy).toEqual({});
    +    await expect(
    +      assertBrowserNavigationAllowed({
    +        url: "http://172.29.128.1/",
    +        ssrfPolicy: browserPolicy,
    +      }),
    +    ).rejects.toThrow(/private\/internal\/special-use ip address/i);
    +  });
    +
    +  it("merges the selected remote profile CDP host with existing CDP policy hostnames", () => {
    +    const profile = createProfile({});
    +
    +    expect(
    +      resolveCdpReachabilityPolicy(profile, {
    +        allowedHostnames: ["metadata.internal"],
    +      }),
    +    ).toEqual({
    +      allowedHostnames: ["metadata.internal", "172.29.128.1"],
    +    });
    +  });
    +
    +  it("keeps local managed loopback CDP control outside browser SSRF policy", () => {
    +    const profile = createProfile({
    +      cdpUrl: "http://127.0.0.1:18800",
    +      cdpHost: "127.0.0.1",
    +      cdpIsLoopback: true,
    +    });
    +
    +    expect(resolveCdpReachabilityPolicy(profile, {})).toBeUndefined();
    +  });
    +});
    
  • extensions/browser/src/browser/cdp-reachability-policy.ts+20 2 modified
    @@ -1,7 +1,25 @@
    -import type { SsrFPolicy } from "../infra/net/ssrf.js";
    +import { isPrivateNetworkAllowedByPolicy, type SsrFPolicy } from "../infra/net/ssrf.js";
     import type { ResolvedBrowserProfile } from "./config.js";
     import { getBrowserProfileCapabilities } from "./profile-capabilities.js";
     
    +function withCdpHostnameAllowed(
    +  profile: ResolvedBrowserProfile,
    +  ssrfPolicy?: SsrFPolicy,
    +): SsrFPolicy | undefined {
    +  if (!ssrfPolicy || !profile.cdpHost) {
    +    return ssrfPolicy;
    +  }
    +  if (isPrivateNetworkAllowedByPolicy(ssrfPolicy)) {
    +    return ssrfPolicy;
    +  }
    +  return {
    +    ...ssrfPolicy,
    +    allowedHostnames: Array.from(
    +      new Set([...(ssrfPolicy.allowedHostnames ?? []), profile.cdpHost]),
    +    ),
    +  };
    +}
    +
     export function resolveCdpReachabilityPolicy(
       profile: ResolvedBrowserProfile,
       ssrfPolicy?: SsrFPolicy,
    @@ -13,7 +31,7 @@ export function resolveCdpReachabilityPolicy(
       if (!capabilities.isRemote && profile.cdpIsLoopback && profile.driver === "openclaw") {
         return undefined;
       }
    -  return ssrfPolicy;
    +  return withCdpHostnameAllowed(profile, ssrfPolicy);
     }
     
     export const resolveCdpControlPolicy = resolveCdpReachabilityPolicy;
    
  • extensions/browser/src/browser/config.test.ts+2 36 modified
    @@ -343,7 +343,7 @@ describe("browser config", () => {
         });
       });
     
    -  it("auto-allowlists hostnames from user-configured profile cdpUrls", () => {
    +  it("keeps configured profile cdpUrls out of the shared browser SSRF policy", () => {
         const resolved = resolveBrowserConfig({
           profiles: {
             remote: {
    @@ -352,41 +352,7 @@ describe("browser config", () => {
             },
           },
         });
    -    expect(resolved.ssrfPolicy).toEqual({
    -      allowedHostnames: ["172.29.128.1"],
    -    });
    -  });
    -
    -  it("merges configured profile cdpUrl hostnames with existing ssrfPolicy allowedHostnames", () => {
    -    const resolved = resolveBrowserConfig({
    -      ssrfPolicy: {
    -        allowedHostnames: ["metadata.internal"],
    -      },
    -      profiles: {
    -        remote: {
    -          color: "#123456",
    -          cdpUrl: "http://172.29.128.1:9223",
    -        },
    -      },
    -    });
    -    expect(resolved.ssrfPolicy?.allowedHostnames?.toSorted()).toEqual(
    -      ["172.29.128.1", "metadata.internal"].toSorted(),
    -    );
    -  });
    -
    -  it("does not duplicate hostnames already in allowedHostnames", () => {
    -    const resolved = resolveBrowserConfig({
    -      ssrfPolicy: {
    -        allowedHostnames: ["172.29.128.1"],
    -      },
    -      profiles: {
    -        remote: {
    -          color: "#123456",
    -          cdpUrl: "http://172.29.128.1:9223",
    -        },
    -      },
    -    });
    -    expect(resolved.ssrfPolicy?.allowedHostnames).toEqual(["172.29.128.1"]);
    +    expect(resolved.ssrfPolicy).toEqual({});
       });
     
       it("resolves existing-session profiles without cdpPort or cdpUrl", () => {
    
  • extensions/browser/src/browser/config.ts+3 43 modified
    @@ -126,27 +126,11 @@ function resolveCdpPortRangeStart(
     
     const normalizeStringList = normalizeOptionalTrimmedStringList;
     
    -function mergeAllowedHostnames(
    -  base: string[] | undefined,
    -  extra: readonly string[],
    -): string[] | undefined {
    -  if (extra.length === 0) {
    -    return base;
    -  }
    -  return Array.from(new Set([...(base ?? []), ...extra]));
    -}
    -
    -function resolveBrowserSsrFPolicy(
    -  cfg: BrowserConfig | undefined,
    -  extraAllowedHostnames: readonly string[] = [],
    -): SsrFPolicy | undefined {
    +function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
       const rawPolicy = cfg?.ssrfPolicy as BrowserSsrFPolicyCompat | undefined;
       const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork;
       const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork;
    -  const allowedHostnames = mergeAllowedHostnames(
    -    normalizeStringList(rawPolicy?.allowedHostnames),
    -    extraAllowedHostnames,
    -  );
    +  const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames);
       const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist);
       const hasExplicitPrivateSetting =
         allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined;
    @@ -175,30 +159,6 @@ function resolveBrowserSsrFPolicy(
       };
     }
     
    -function collectConfiguredCdpHostnames(cfg: BrowserConfig | undefined): string[] {
    -  const hostnames = new Set<string>();
    -  const addHostnameFromUrl = (rawUrl: string | undefined): void => {
    -    const trimmed = rawUrl?.trim() ?? "";
    -    if (!trimmed) {
    -      return;
    -    }
    -    try {
    -      const hostname = new URL(trimmed).hostname;
    -      if (hostname) {
    -        hostnames.add(hostname);
    -      }
    -    } catch {
    -      // Ignore unparseable URLs; they will be rejected elsewhere with a proper error.
    -    }
    -  };
    -
    -  addHostnameFromUrl(cfg?.cdpUrl);
    -  for (const profile of Object.values(cfg?.profiles ?? {})) {
    -    addHostnameFromUrl(profile?.cdpUrl);
    -  }
    -  return Array.from(hostnames);
    -}
    -
     function ensureDefaultProfile(
       profiles: Record<string, BrowserProfileConfig> | undefined,
       defaultColor: string,
    @@ -333,7 +293,7 @@ export function resolveBrowserConfig(
         attachOnly,
         defaultProfile,
         profiles,
    -    ssrfPolicy: resolveBrowserSsrFPolicy(cfg, collectConfiguredCdpHostnames(cfg)),
    +    ssrfPolicy: resolveBrowserSsrFPolicy(cfg),
         extraArgs,
       };
     }
    
  • extensions/browser/src/browser/server-context.loopback-direct-ws.test.ts+5 2 modified
    @@ -143,14 +143,17 @@ describe("browser server-context loopback direct WebSocket profiles", () => {
         await openclaw.closeTab("T2");
       });
     
    -  it("blocks direct WebSocket tab operations when strict SSRF policy rejects the cdpUrl", async () => {
    +  it("blocks direct WebSocket tab operations when strict SSRF hostname allowlist rejects the cdpUrl", async () => {
         const fetchMock = vi.fn(async () => {
           throw new Error("unexpected fetch");
         });
     
         global.fetch = withFetchPreconnect(fetchMock);
         const state = makeState("openclaw");
    -    state.resolved.ssrfPolicy = { dangerouslyAllowPrivateNetwork: false };
    +    state.resolved.ssrfPolicy = {
    +      dangerouslyAllowPrivateNetwork: false,
    +      hostnameAllowlist: ["browserless.example.com"],
    +    };
         state.resolved.profiles.openclaw = {
           cdpUrl: "ws://10.0.0.42:18800/devtools/browser/SESSION?token=abc",
           color: "#FF4500",
    
  • extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts+5 2 modified
    @@ -149,7 +149,7 @@ describe("browser remote profile tab ops via Playwright", () => {
         expect(state.profiles.get("remote")?.lastTargetId).toBe("T1");
       });
     
    -  it("blocks remote Playwright tab operations when strict SSRF policy rejects the cdpUrl", async () => {
    +  it("blocks remote Playwright tab operations when strict SSRF hostname allowlist rejects the cdpUrl", async () => {
         const listPagesViaPlaywright = vi.fn(async () => [
           { targetId: "T1", title: "Tab 1", url: "https://example.com", type: "page" },
         ]);
    @@ -163,7 +163,10 @@ describe("browser remote profile tab ops via Playwright", () => {
         } as unknown as Awaited<ReturnType<typeof deps.pwAiModule.getPwAiModule>>);
     
         const state = deps.makeState("remote");
    -    state.resolved.ssrfPolicy = { dangerouslyAllowPrivateNetwork: false };
    +    state.resolved.ssrfPolicy = {
    +      dangerouslyAllowPrivateNetwork: false,
    +      hostnameAllowlist: ["browserless.example.com"],
    +    };
         state.resolved.profiles.remote = {
           ...state.resolved.profiles.remote,
           cdpUrl: "http://10.0.0.42:9222",
    
e90c89cf8b14

fix(browser): auto-allowlist configured CDP hostnames in SSRF policy

https://github.com/openclaw/openclawHansYApr 17, 2026via nvd-ref
2 files changed · +89 3
  • extensions/browser/src/browser/config.test.ts+46 0 modified
    @@ -343,6 +343,52 @@ describe("browser config", () => {
         });
       });
     
    +  it("auto-allowlists hostnames from user-configured profile cdpUrls", () => {
    +    const resolved = resolveBrowserConfig({
    +      profiles: {
    +        remote: {
    +          color: "#123456",
    +          cdpUrl: "http://172.29.128.1:9223",
    +        },
    +      },
    +    });
    +    expect(resolved.ssrfPolicy).toEqual({
    +      allowedHostnames: ["172.29.128.1"],
    +    });
    +  });
    +
    +  it("merges configured profile cdpUrl hostnames with existing ssrfPolicy allowedHostnames", () => {
    +    const resolved = resolveBrowserConfig({
    +      ssrfPolicy: {
    +        allowedHostnames: ["metadata.internal"],
    +      },
    +      profiles: {
    +        remote: {
    +          color: "#123456",
    +          cdpUrl: "http://172.29.128.1:9223",
    +        },
    +      },
    +    });
    +    expect(resolved.ssrfPolicy?.allowedHostnames?.toSorted()).toEqual(
    +      ["172.29.128.1", "metadata.internal"].toSorted(),
    +    );
    +  });
    +
    +  it("does not duplicate hostnames already in allowedHostnames", () => {
    +    const resolved = resolveBrowserConfig({
    +      ssrfPolicy: {
    +        allowedHostnames: ["172.29.128.1"],
    +      },
    +      profiles: {
    +        remote: {
    +          color: "#123456",
    +          cdpUrl: "http://172.29.128.1:9223",
    +        },
    +      },
    +    });
    +    expect(resolved.ssrfPolicy?.allowedHostnames).toEqual(["172.29.128.1"]);
    +  });
    +
       it("resolves existing-session profiles without cdpPort or cdpUrl", () => {
         const resolved = resolveBrowserConfig({
           profiles: {
    
  • extensions/browser/src/browser/config.ts+43 3 modified
    @@ -126,11 +126,27 @@ function resolveCdpPortRangeStart(
     
     const normalizeStringList = normalizeOptionalTrimmedStringList;
     
    -function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
    +function mergeAllowedHostnames(
    +  base: string[] | undefined,
    +  extra: readonly string[],
    +): string[] | undefined {
    +  if (extra.length === 0) {
    +    return base;
    +  }
    +  return Array.from(new Set([...(base ?? []), ...extra]));
    +}
    +
    +function resolveBrowserSsrFPolicy(
    +  cfg: BrowserConfig | undefined,
    +  extraAllowedHostnames: readonly string[] = [],
    +): SsrFPolicy | undefined {
       const rawPolicy = cfg?.ssrfPolicy as BrowserSsrFPolicyCompat | undefined;
       const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork;
       const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork;
    -  const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames);
    +  const allowedHostnames = mergeAllowedHostnames(
    +    normalizeStringList(rawPolicy?.allowedHostnames),
    +    extraAllowedHostnames,
    +  );
       const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist);
       const hasExplicitPrivateSetting =
         allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined;
    @@ -159,6 +175,30 @@ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy |
       };
     }
     
    +function collectConfiguredCdpHostnames(cfg: BrowserConfig | undefined): string[] {
    +  const hostnames = new Set<string>();
    +  const addHostnameFromUrl = (rawUrl: string | undefined): void => {
    +    const trimmed = rawUrl?.trim() ?? "";
    +    if (!trimmed) {
    +      return;
    +    }
    +    try {
    +      const hostname = new URL(trimmed).hostname;
    +      if (hostname) {
    +        hostnames.add(hostname);
    +      }
    +    } catch {
    +      // Ignore unparseable URLs; they will be rejected elsewhere with a proper error.
    +    }
    +  };
    +
    +  addHostnameFromUrl(cfg?.cdpUrl);
    +  for (const profile of Object.values(cfg?.profiles ?? {})) {
    +    addHostnameFromUrl(profile?.cdpUrl);
    +  }
    +  return Array.from(hostnames);
    +}
    +
     function ensureDefaultProfile(
       profiles: Record<string, BrowserProfileConfig> | undefined,
       defaultColor: string,
    @@ -293,7 +333,7 @@ export function resolveBrowserConfig(
         attachOnly,
         defaultProfile,
         profiles,
    -    ssrfPolicy: resolveBrowserSsrFPolicy(cfg),
    +    ssrfPolicy: resolveBrowserSsrFPolicy(cfg, collectConfiguredCdpHostnames(cfg)),
         extraArgs,
       };
     }
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

4

News mentions

0

No linked articles in our index yet.