High severity7.7GHSA Advisory· Published May 5, 2026· Updated May 7, 2026
CVE-2026-43527
CVE-2026-43527
Description
OpenClaw before 2026.4.14 contains a server-side request forgery vulnerability in browser SSRF policy that allows private-network navigation by default. Attackers can exploit this misconfiguration to access internal services or metadata endpoints through browser-driven requests.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.14 | 2026.4.14 |
Affected products
2Patches
47eecfa411df3fix(browser): unblock loopback CDP readiness under strict SSRF defaults (#66354)
8 files changed · +248 −9
CHANGELOG.md+1 −0 modified@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai - Browser/SSRF: restore hostname navigation under the default browser SSRF policy while keeping explicit strict mode reachable from config, and keep managed loopback CDP `/json/new` fallback requests on the local CDP control policy so browser follow-up fixes stop regressing normal navigation or self-blocking local CDP control. (#66386) Thanks @obviyus. - Browser/SSRF: preserve explicit strict browser navigation mode for legacy `browser.ssrfPolicy.allowPrivateNetwork: false` configs by normalizing the legacy alias to the canonical strict marker instead of silently widening those installs to the default non-strict hostname-navigation path. - Agents/subagents: emit the subagent registry lazy-runtime stub on the stable dist path that both source and bundled runtime imports resolve, so the follow-up dist fix no longer still fails with `ERR_MODULE_NOT_FOUND` at runtime. (#66420) Thanks @obviyus. +- Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819. ## 2026.4.14-beta.1
docs/cli/browser.md+14 −0 modified@@ -33,6 +33,20 @@ openclaw browser --browser-profile openclaw open https://example.com openclaw browser --browser-profile openclaw snapshot ``` +## Quick troubleshooting + +If `start` fails with `not reachable after start`, troubleshoot CDP readiness first. If `start` and `tabs` succeed but `open` or `navigate` fails, the browser control plane is healthy and the failure is usually navigation SSRF policy. + +Minimal sequence: + +```bash +openclaw browser --browser-profile openclaw start +openclaw browser --browser-profile openclaw tabs +openclaw browser --browser-profile openclaw open https://example.com +``` + +Detailed guidance: [Browser troubleshooting](/tools/browser#cdp-startup-failure-vs-navigation-ssrf-block) + ## Lifecycle ```bash
docs/tools/browser.md+57 −0 modified@@ -884,6 +884,63 @@ For Linux-specific issues (especially snap Chromium), see For WSL2 Gateway + Windows Chrome split-host setups, see [WSL2 + Windows + remote Chrome CDP troubleshooting](/tools/browser-wsl2-windows-remote-cdp-troubleshooting). +### CDP startup failure vs navigation SSRF block + +These are different failure classes and they point to different code paths. + +- **CDP startup or readiness failure** means OpenClaw cannot confirm that the browser control plane is healthy. +- **Navigation SSRF block** means the browser control plane is healthy, but a page navigation target is rejected by policy. + +Common examples: + +- CDP startup or readiness failure: + - `Chrome CDP websocket for profile "openclaw" is not reachable after start` + - `Remote CDP for profile "<name>" is not reachable at <cdpUrl>` +- Navigation SSRF block: + - `open`, `navigate`, snapshot, or tab-opening flows fail with a browser/network policy error while `start` and `tabs` still work + +Use this minimal sequence to separate the two: + +```bash +openclaw browser --browser-profile openclaw start +openclaw browser --browser-profile openclaw tabs +openclaw browser --browser-profile openclaw open https://example.com +``` + +How to read the results: + +- If `start` fails with `not reachable after start`, troubleshoot CDP readiness first. +- If `start` succeeds but `tabs` fails, the control plane is still unhealthy. Treat this as a CDP reachability problem, not a page-navigation problem. +- If `start` and `tabs` succeed but `open` or `navigate` fails, the browser control plane is up and the failure is in navigation policy or the target page. +- If `start`, `tabs`, and `open` all succeed, the basic managed-browser control path is healthy. + +Important behavior details: + +- Browser config defaults to a fail-closed SSRF policy object even when you do not configure `browser.ssrfPolicy`. +- For the local loopback `openclaw` managed profile, CDP health checks intentionally skip browser SSRF reachability enforcement for OpenClaw's own local control plane. +- Navigation protection is separate. A successful `start` or `tabs` result does not mean a later `open` or `navigate` target is allowed. + +Security guidance: + +- Do **not** relax browser SSRF policy by default. +- Prefer narrow host exceptions such as `hostnameAllowlist` or `allowedHostnames` over broad private-network access. +- Use `dangerouslyAllowPrivateNetwork: true` only in intentionally trusted environments where private-network browser access is required and reviewed. + +Example: navigation blocked, control plane healthy + +- `start` succeeds +- `tabs` succeeds +- `open http://internal.example` fails + +That usually means browser startup is fine and the navigation target needs policy review. + +Example: startup blocked before navigation matters + +- `start` fails with `not reachable after start` +- `tabs` also fails or cannot run + +That points to browser launch or CDP reachability, not a page URL allowlist problem. + ## Agent tools + how control works The agent gets **one tool** for browser automation:
extensions/browser/src/browser/cdp.helpers.test.ts+76 −1 modified@@ -10,7 +10,7 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => { }; }); -import { fetchJson, fetchOk } from "./cdp.helpers.js"; +import { assertCdpEndpointAllowed, fetchJson, fetchOk } from "./cdp.helpers.js"; describe("cdp helpers", () => { afterEach(() => { @@ -43,6 +43,23 @@ describe("cdp helpers", () => { expect(release).toHaveBeenCalledTimes(1); }); + it("allows loopback CDP endpoints in strict SSRF mode", async () => { + await expect( + assertCdpEndpointAllowed("http://127.0.0.1:9222/json/version", { + dangerouslyAllowPrivateNetwork: false, + }), + ).resolves.toBeUndefined(); + }); + + it("still enforces hostname allowlist for loopback CDP endpoints", async () => { + await expect( + assertCdpEndpointAllowed("http://127.0.0.1:9222/json/version", { + dangerouslyAllowPrivateNetwork: false, + hostnameAllowlist: ["*.corp.example"], + }), + ).rejects.toThrow("browser endpoint blocked by policy"); + }); + it("releases guarded CDP fetches for bodyless requests", async () => { const release = vi.fn(async () => {}); fetchWithSsrFGuardMock.mockResolvedValueOnce({ @@ -62,4 +79,62 @@ describe("cdp helpers", () => { expect(release).toHaveBeenCalledTimes(1); }); + + it("uses an exact loopback allowlist for guarded loopback CDP fetches", async () => { + const release = vi.fn(async () => {}); + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: { + ok: true, + status: 200, + }, + release, + }); + + await expect( + fetchOk("http://127.0.0.1:9222/json/version", 250, undefined, { + dangerouslyAllowPrivateNetwork: false, + }), + ).resolves.toBeUndefined(); + + expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith( + expect.objectContaining({ + url: "http://127.0.0.1:9222/json/version", + policy: { + dangerouslyAllowPrivateNetwork: false, + allowedHostnames: ["127.0.0.1"], + }, + }), + ); + expect(release).toHaveBeenCalledTimes(1); + }); + + it("preserves hostname allowlist while allowing exact loopback CDP fetches", async () => { + const release = vi.fn(async () => {}); + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: { + ok: true, + status: 200, + }, + release, + }); + + await expect( + fetchOk("http://127.0.0.1:9222/json/version", 250, undefined, { + dangerouslyAllowPrivateNetwork: false, + hostnameAllowlist: ["*.corp.example"], + }), + ).resolves.toBeUndefined(); + + expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith( + expect.objectContaining({ + url: "http://127.0.0.1:9222/json/version", + policy: { + dangerouslyAllowPrivateNetwork: false, + hostnameAllowlist: ["*.corp.example"], + allowedHostnames: ["127.0.0.1"], + }, + }), + ); + expect(release).toHaveBeenCalledTimes(1); + }); });
extensions/browser/src/browser/cdp.helpers.ts+19 −2 modified@@ -69,8 +69,16 @@ export async function assertCdpEndpointAllowed( throw new Error(`Invalid CDP URL protocol: ${parsed.protocol.replace(":", "")}`); } try { + const policy = isLoopbackHost(parsed.hostname) + ? { + ...ssrfPolicy, + allowedHostnames: Array.from( + new Set([...(ssrfPolicy?.allowedHostnames ?? []), parsed.hostname]), + ), + } + : ssrfPolicy; await resolvePinnedHostnameWithPolicy(parsed.hostname, { - policy: ssrfPolicy, + policy, }); } catch (error) { throw new BrowserCdpEndpointBlockedError({ cause: error }); @@ -263,11 +271,20 @@ export async function fetchCdpChecked( try { const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {}); const res = await withNoProxyForCdpUrl(url, async () => { + const parsedUrl = new URL(url); + const policy = isLoopbackHost(parsedUrl.hostname) + ? { + ...ssrfPolicy, + allowedHostnames: Array.from( + new Set([...(ssrfPolicy?.allowedHostnames ?? []), parsedUrl.hostname]), + ), + } + : (ssrfPolicy ?? { allowPrivateNetwork: true }); const guarded = await fetchWithSsrFGuard({ url, init: { ...init, headers }, signal: ctrl.signal, - policy: ssrfPolicy ?? { allowPrivateNetwork: true }, + policy, auditContext: "browser-cdp", }); guardedRelease = guarded.release;
extensions/browser/src/browser/chrome.loopback-ssrf.integration.test.ts+70 −0 added@@ -0,0 +1,70 @@ +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterEach, describe, expect, it } from "vitest"; +import { getChromeWebSocketUrl, isChromeReachable } from "./chrome.js"; + +type RunningServer = { + server: Server; + baseUrl: string; +}; + +const runningServers: Server[] = []; + +async function startLoopbackCdpServer(): Promise<RunningServer> { + const server = createServer((req, res) => { + if (req.url !== "/json/version") { + res.statusCode = 404; + res.end("not found"); + return; + } + const address = server.address() as AddressInfo; + res.setHeader("content-type", "application/json"); + res.end( + JSON.stringify({ + Browser: "Chrome/999.0.0.0", + webSocketDebuggerUrl: `ws://127.0.0.1:${address.port}/devtools/browser/TEST`, + }), + ); + }); + + await new Promise<void>((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => resolve()); + }); + + runningServers.push(server); + const address = server.address() as AddressInfo; + return { + server, + baseUrl: `http://127.0.0.1:${address.port}`, + }; +} + +afterEach(async () => { + await Promise.all( + runningServers + .splice(0) + .map( + (server) => + new Promise<void>((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + ), + ); +}); + +describe("chrome loopback SSRF integration", () => { + it("keeps loopback CDP HTTP reachability working under strict default SSRF policy", async () => { + const { baseUrl } = await startLoopbackCdpServer(); + + await expect(isChromeReachable(baseUrl, 500, {})).resolves.toBe(true); + }); + + it("returns the loopback websocket URL under strict default SSRF policy", async () => { + const { baseUrl } = await startLoopbackCdpServer(); + + await expect(getChromeWebSocketUrl(baseUrl, 500, {})).resolves.toMatch( + /\/devtools\/browser\/TEST$/, + ); + }); +});
extensions/browser/src/browser/chrome.test.ts+11 −5 modified@@ -312,22 +312,28 @@ describe("browser chrome helpers", () => { await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); }); - it("blocks private CDP probes when strict SSRF policy is enabled", async () => { - const fetchSpy = vi.fn().mockRejectedValue(new Error("should not be called")); + it("allows loopback CDP probes while still blocking non-loopback private targets in strict SSRF mode", async () => { + const fetchSpy = vi + .fn() + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ webSocketDebuggerUrl: "ws://127.0.0.1/devtools" }), + } as unknown as Response) + .mockRejectedValue(new Error("should not be called")); vi.stubGlobal("fetch", fetchSpy); await expect( isChromeReachable("http://127.0.0.1:12345", 50, { dangerouslyAllowPrivateNetwork: false, }), - ).resolves.toBe(false); + ).resolves.toBe(true); await expect( - isChromeReachable("ws://127.0.0.1:19999", 50, { + isChromeReachable("http://169.254.169.254:12345", 50, { dangerouslyAllowPrivateNetwork: false, }), ).resolves.toBe(false); - expect(fetchSpy).not.toHaveBeenCalled(); + expect(fetchSpy).toHaveBeenCalledTimes(1); }); it("blocks cross-host websocket pivots returned by /json/version in strict SSRF mode", async () => {
extensions/browser/src/browser/server-context.availability.ts+0 −1 modified@@ -71,7 +71,6 @@ export function createProfileAvailability({ const getCdpReachabilityPolicy = () => resolveCdpReachabilityPolicy(profile, state().resolved.ssrfPolicy); - const isReachable = async (timeoutMs?: number) => { if (capabilities.usesChromeMcp) { // listChromeMcpTabs creates the session if needed — no separate ensureChromeMcpAvailable call required
213c36cf5112fix(browser): preserve legacy strict SSRF alias
3 files changed · +13 −1
CHANGELOG.md+1 −0 modified@@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai - Doctor/plugins: cache external `preferOver` catalog lookups within each plugin auto-enable pass so large `agents.list` configs no longer peg CPU and repeatedly reread plugin catalogs during doctor/plugins resolution. (#66246) Thanks @yfge. - Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when `agents.defaults.contextTokens` is the real limit. (#66236) Thanks @ImLukeF. - Browser/SSRF: restore hostname navigation under the default browser SSRF policy while keeping explicit strict mode reachable from config, and keep managed loopback CDP `/json/new` fallback requests on the local CDP control policy so browser follow-up fixes stop regressing normal navigation or self-blocking local CDP control. (#66386) Thanks @obviyus. +- Browser/SSRF: preserve explicit strict browser navigation mode for legacy `browser.ssrfPolicy.allowPrivateNetwork: false` configs by normalizing the legacy alias to the canonical strict marker instead of silently widening those installs to the default non-strict hostname-navigation path. ## 2026.4.14-beta.1
extensions/browser/src/browser/config.test.ts+9 −0 modified@@ -321,6 +321,15 @@ describe("browser config", () => { expect(resolved.ssrfPolicy).toEqual({ dangerouslyAllowPrivateNetwork: false }); }); + it("preserves legacy explicit strict mode from allowPrivateNetwork=false", () => { + const resolved = resolveBrowserConfig({ + ssrfPolicy: { + allowPrivateNetwork: false, + }, + } as unknown as BrowserConfig); + expect(resolved.ssrfPolicy).toEqual({ dangerouslyAllowPrivateNetwork: false }); + }); + it("keeps allowlist-only browser SSRF policy strict by default", () => { const resolved = resolveBrowserConfig({ ssrfPolicy: {
extensions/browser/src/browser/config.ts+3 −1 modified@@ -149,7 +149,9 @@ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | } return { - ...(resolvedAllowPrivateNetwork || dangerouslyAllowPrivateNetwork === false + ...(resolvedAllowPrivateNetwork || + dangerouslyAllowPrivateNetwork === false || + allowPrivateNetwork === false ? { dangerouslyAllowPrivateNetwork: resolvedAllowPrivateNetwork } : {}), ...(allowedHostnames ? { allowedHostnames } : {}),
024f4614a1a1fix: add browser SSRF follow-up changelog entry (#66386)
1 file changed · +1 −0
CHANGELOG.md+1 −0 modified@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai - Doctor/systemd: keep `openclaw doctor --repair` and service reinstall from re-embedding dotenv-backed secrets in user systemd units, while preserving newer inline overrides over stale state-dir `.env` values. (#66249) Thanks @tmimmanuel. - Doctor/plugins: cache external `preferOver` catalog lookups within each plugin auto-enable pass so large `agents.list` configs no longer peg CPU and repeatedly reread plugin catalogs during doctor/plugins resolution. (#66246) Thanks @yfge. - Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when `agents.defaults.contextTokens` is the real limit. (#66236) Thanks @ImLukeF. +- Browser/SSRF: restore hostname navigation under the default browser SSRF policy while keeping explicit strict mode reachable from config, and keep managed loopback CDP `/json/new` fallback requests on the local CDP control policy so browser follow-up fixes stop regressing normal navigation or self-blocking local CDP control. (#66386) Thanks @obviyus. ## 2026.4.14-beta.1
1dabfef28db5fix(browser): preserve explicit strict SSRF config
2 files changed · +4 −2
extensions/browser/src/browser/config.test.ts+1 −1 modified@@ -318,7 +318,7 @@ describe("browser config", () => { dangerouslyAllowPrivateNetwork: false, }, }); - expect(resolved.ssrfPolicy).toEqual({}); + expect(resolved.ssrfPolicy).toEqual({ dangerouslyAllowPrivateNetwork: false }); }); it("keeps allowlist-only browser SSRF policy strict by default", () => {
extensions/browser/src/browser/config.ts+3 −1 modified@@ -149,7 +149,9 @@ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | } return { - ...(resolvedAllowPrivateNetwork ? { dangerouslyAllowPrivateNetwork: true } : {}), + ...(resolvedAllowPrivateNetwork || dangerouslyAllowPrivateNetwork === false + ? { dangerouslyAllowPrivateNetwork: resolvedAllowPrivateNetwork } + : {}), ...(allowedHostnames ? { allowedHostnames } : {}), ...(hostnameAllowlist ? { hostnameAllowlist } : {}), };
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
10- github.com/openclaw/openclaw/commit/024f4614a1a1831406e763adc40ef226e3d5e9ednvdPatchWEB
- github.com/openclaw/openclaw/commit/1dabfef28db523e7de81edeb3dd689e9171236a2nvdPatchWEB
- github.com/openclaw/openclaw/commit/213c36cf51121ef6c05cfccd78037371f968f31anvdPatchWEB
- github.com/openclaw/openclaw/commit/7eecfa411df3d12e6b810e6ca5df47254fc3db3fnvdPatchWEB
- github.com/advisories/GHSA-53vx-pmqw-863cghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-53vx-pmqw-863cnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43527ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-server-side-request-forgery-via-private-network-navigationnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/pull/66354ghsaWEB
- github.com/openclaw/openclaw/pull/66386ghsaWEB
News mentions
0No linked articles in our index yet.