VYPR
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.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.142026.4.14

Affected products

2
  • OpenClaw/OpenclawGHSA2 versions
    < 2026.4.14+ 1 more
    • (no CPE)range: < 2026.4.14
    • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*range: <2026.4.14

Patches

4
7eecfa411df3

fix(browser): unblock loopback CDP readiness under strict SSRF defaults (#66354)

https://github.com/openclaw/openclawMason HuangApr 14, 2026via ghsa
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
    
213c36cf5112

fix(browser): preserve legacy strict SSRF alias

https://github.com/openclaw/openclawAyaan ZaidiApr 14, 2026via ghsa
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 } : {}),
    
024f4614a1a1

fix: add browser SSRF follow-up changelog entry (#66386)

https://github.com/openclaw/openclawAyaan ZaidiApr 14, 2026via ghsa
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
     
    
1dabfef28db5

fix(browser): preserve explicit strict SSRF config

https://github.com/openclaw/openclawAyaan ZaidiApr 14, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.