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
1Patches
21fd049e3074cfix: scope remote CDP host allowlist (#68207)
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",
e90c89cf8b14fix(browser): auto-allowlist configured CDP hostnames in SSRF policy
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- github.com/openclaw/openclaw/commit/1fd049e3074cac72f6734a7fe88468c84f5f8bd7nvdPatch
- github.com/openclaw/openclaw/commit/e90c89cf8b1459f2aa1f3a665be67392b6c03fdfnvdPatch
- github.com/openclaw/openclaw/security/advisories/GHSA-j4c5-89f5-f3pmnvdThird Party Advisory
- www.vulncheck.com/advisories/openclaw-server-side-request-forgery-via-browser-cdp-profile-creationnvdThird Party AdvisoryPatch
News mentions
0No linked articles in our index yet.