Skip to content

[Bug]: Managed loopback browser profile fails ensureBrowserAvailable because agent SSRF policy is applied to its own CDP endpoint #66170

@Smith4545

Description

@Smith4545

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

OpenClaw's managed browser profile on native Linux cannot start or attach to its own Chrome instance when any form of SSRF strict mode is active in browser.ssrfPolicy. Chrome starts correctly and binds CDP on 127.0.0.1, /json/version returns a valid response including webSocketDebuggerUrl, and the port is reachable via curl and ss, but OpenClaw itself reports the profile as running: false / browser: unknown and refuses any browser operation.

Root cause is that the agent-facing ssrfPolicy is passed to the internal reachability probes in createProfileAvailability and applied to OpenClaw's own loopback profile.cdpUrl. The SSRF policy is intended to guard agent-initiated navigation, not the gateway's own lifecycle operations against its own managed browser.

Likely the same underlying bug as #64900 (WSL2 + Windows Edge remote CDP), but reproduced on a completely different platform with a local managed profile, which narrows it down to the gateway-side reachability logic rather than anything WSL/network-bridge specific.

Steps to reproduce

Environment:

  • OpenClaw 2026.4.11 (769908e)
  • Gentoo Linux (native, no containers, no WSL)
  • Google Chrome 147.0.7727.55 (Gentoo binpkg — official .deb repackaged, not Snap/Flatpak)
  • Node via npm global install
  • Default plugins.entries.browser.enabled: true

Config ~/.openclaw/openclaw.json:

{
  "browser": {
    "enabled": true,
    "defaultProfile": "openclaw",
    "ssrfPolicy": {
      "hostnameAllowlist": ["*.example.com", "example.com"]
    },
    "profiles": {
      "openclaw": {
        "cdpPort": 18800,
        "cdpUrl": "http://127.0.0.1:18800",
        "color": "#FF4500"
      }
    }
  }
}

(hostnameAllowlist is set as suggested by the "Strict-mode example" in the browser docs.)

Reproducer:

openclaw browser --browser-profile openclaw start

Expected behavior

OpenClaw launches Chrome on 127.0.0.1:18800, detects the CDP endpoint as reachable, profile status becomes running: true, subsequent open/snapshot/tabs calls work.

Actual behavior

After exactly ~8.4 seconds:

GatewayClientRequestError: Error: Chrome CDP websocket for profile "openclaw" is not reachable after start.

The ~8.4s delay matches the waitForCdpReadyAfterLaunch poll window in server-context.availability.ts exhausting its deadline. OpenClaw then kills the Chrome process it spawned.

Meanwhile, from an external shell while Chrome is alive (before OpenClaw kills it):

$ curl -s http://127.0.0.1:18800/json/version
{
   "Browser": "Chrome/147.0.7727.55",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) ...",
   "V8-Version": "14.7.173.16",
   "WebKit-Version": "537.36 (@7dc2b8f6f651b42c0a8f3634c9feb5e0b6b25c91)",
   "webSocketDebuggerUrl": "ws://127.0.0.1:18800/devtools/browser/<uuid>"
}
 
$ ss -tlnp | grep 18800
LISTEN 0 10 127.0.0.1:18800 0.0.0.0:* users:(("chrome",pid=3680970,fd=92))

The CDP endpoint is objectively reachable — OpenClaw itself refuses to connect to its own child process.

Also reproduces symmetrically with attachOnly: true against a manually-launched Chrome (systemd user unit, same flags OpenClaw would use):

openclaw browser --browser-profile openclaw status
  profile: openclaw
  enabled: true
  running: false              # ← despite curl/ss showing Chrome alive on 127.0.0.1:18800
  transport: cdp
  cdpPort: 18800
  cdpUrl: http://127.0.0.1:18800
  browser: unknown            # ← OpenClaw doesn't recognize its own-configured endpoint
  detectedBrowser: chrome
  detectedPath: /usr/bin/google-chrome-stable
 
openclaw browser --browser-profile openclaw open https://1.1.1.1
  GatewayClientRequestError: Browser attachOnly is enabled and profile "openclaw" is not running.

In extensions/browser/src/browser/server-context.availability.ts (built file: server-context-*.js, around line 3440–3460 in 2026.4.11):

function createProfileAvailability({ opts, profile, state, getProfileState, setProfileRunning }) {
  const capabilities = getBrowserProfileCapabilities(profile);
  const resolveTimeouts = (timeoutMs) => resolveCdpReachabilityTimeouts({
    profileIsLoopback: profile.cdpIsLoopback,
    ...
  });
  const isReachable = async (timeoutMs) => {
    if (capabilities.usesChromeMcp) { ... }
    const { httpTimeoutMs, wsTimeoutMs } = resolveTimeouts(timeoutMs);
    return await isChromeCdpReady(profile.cdpUrl, httpTimeoutMs, wsTimeoutMs, state().resolved.ssrfPolicy);
  };
  const isHttpReachable = async (timeoutMs) => {
    if (capabilities.usesChromeMcp) return await isReachable(timeoutMs);
    const { httpTimeoutMs } = resolveTimeouts(timeoutMs);
    return await isChromeReachable(profile.cdpUrl, httpTimeoutMs, state().resolved.ssrfPolicy);
  };

Both isReachable and isHttpReachable pass state().resolved.ssrfPolicy as the third argument to the underlying probe functions (isChromeCdpReady, isChromeReachable, imported from chrome-*.js). These are internal probes against profile.cdpUrl — OpenClaw's own managed-browser endpoint.

When any strict-mode condition is active in the SSRF policy, 127.0.0.1 fails the probe. Two independent ways to hit this, both realistic:

  1. Explicit strict mode: user sets hostnameAllowlist (which the docs actively recommend as the "Strict-mode example"). IPv4 literals don't match hostname globs, so the loopback probe is rejected.
  2. Implicit strict mode: default dangerouslyAllowPrivateNetwork: false plus any configuration that triggers private-network blocking. 127.0.0.1 is per definition private/loopback.

ensureBrowserAvailable then fails in the isHttpReachable branch (~line 3519), and depending on profile mode either:

  • attachOnly: true → immediate throw at line ~3527: Browser attachOnly is enabled and profile "${name}" is not running.
  • Managed launch → launchOpenClawChromewaitForCdpReadyAfterLaunch polls isReachable for CDP_READY_AFTER_LAUNCH_WINDOW_MS, exhausts the deadline (~8s), throws at line ~3490: Chrome CDP websocket for profile "${name}" is not reachable after start., then stopOpenClawChrome kills the spawned Chrome at line ~3533.

Relevant observation: profile.cdpIsLoopback is already computed and used in an adjacent branch at ~line 3524 for a different control-flow decision. The flag is present — it's just not propagated into the SSRF exemption path, because there is no SSRF exemption path for internal gateway↔managed-browser traffic at all.

The agent-facing SSRF policy exists to protect against an agent being tricked into navigating to an internal/private URL (DNS rebinding, SSRF-style pivot attacks, etc.). It has nothing to do with whether the gateway is allowed to talk to its own explicitly-configured, explicitly-bound managed browser process over loopback. Applying it to internal probes conflates two different threat models:

  • Agent-initiated navigation (untrusted input, needs SSRF guarding) → openclaw browser open <url>
  • Gateway-internal lifecycle operations (trusted input, configured by the operator) → isHttpReachable(profile.cdpUrl)

The second category should never be gated by the first category's policy. The operator already declared intent by configuring browser.profiles.<name>.cdpUrl (or accepting the default loopback). Re-validating that against a security policy that's scoped to untrusted agent input means the operator can't express a meaningful SSRF policy for agent traffic without also breaking the managed browser.

Adding explicit loopback allowances to the SSRF policy makes the managed browser usable again:

{
  "browser": {
    "ssrfPolicy": {
      "dangerouslyAllowPrivateNetwork": true,
      "hostnameAllowlist": ["*"],
      "allowedHostnames": ["127.0.0.1", "localhost"]
    }
  }
}

This works but is misleading - it forces the operator to "dangerously" allow private-network access just to re-enable the gateway's own internal loopback communication, and it weakens the SSRF policy for agent navigation as a side effect of fixing an internal plumbing issue.

OpenClaw version

2026.4.11 (769908e)

Operating system

Gentoo Linux (native, no container, no WSL, no snap/flatpak or other bullshit)

Install method

npm global baby

Model

Not relevant - failure occurs before any agent traffic.

Provider / routing chain

Not relevant - failure is gateway-internal against its own managed browser process on 127.0.0.1.

Additional provider/model setup details

Internal probes issued from createProfileAvailability against profile.cdpUrl should not flow through state().resolved.ssrfPolicy. Instead:

  1. If profile.cdpIsLoopback === true (the flag is already there), skip SSRF checks for isChromeReachable / isChromeCdpReady entirely — loopback to the gateway's own managed-browser process is trust-by-configuration, not by policy.
  2. For remote CDP profiles (explicitly configured cdpUrl on a non-loopback host), either:
    • Still skip the agent SSRF policy, since the operator has explicitly declared this endpoint as trusted by adding it to the profile config, or
    • Apply a separate, infrastructure-scoped SSRF policy (browser.internal.ssrfPolicy or similar) that defaults to permissive for explicitly-configured endpoints.

Alternative minimal fix: keep the SSRF policy application, but always implicitly include the configured profile.cdpUrl host in the effective allowlist at resolution time, so the operator's declared CDP endpoint is self-exempting regardless of broader policy.

Logs, screenshots, and evidence

Gateway log excerpt (managed launch path):
 

19:54:31+00:00 info browser/chrome {"subsystem":"browser/chrome"} 🦞 openclaw browser started (chrome) profile "openclaw" on 127.0.0.1:18800 (pid 3649976)
19:54:39+00:00 info gateway/ws {"subsystem":"gateway/ws"} ⇄ res ✗ browser.request 8460ms errorCode=UNAVAILABLE errorMessage=Error: Chrome CDP websocket for profile "openclaw" is not reachable after start. conn=20f922d0…c17b id=0fd46ed7…701b
19:54:39+00:00 error GatewayClientRequestError: Error: Chrome CDP websocket for profile "openclaw" is not reachable after start

 
Chrome is alive, bound, responsive — OpenClaw refuses to talk to it:
 

$ curl -s http://127.0.0.1:18800/json/version | jq .Browser
"Chrome/147.0.7727.55"
 
$ ss -tlnp | grep 18800
LISTEN 0 10 127.0.0.1:18800 0.0.0.0:* users:(("chrome",pid=3680970,fd=92))
 
$ openclaw browser --browser-profile openclaw status
  running: false
  browser: unknown
  cdpUrl: http://127.0.0.1:18800

 
Stable `~8.4s` delay on every failed managed-launch attempt matches `waitForCdpReadyAfterLaunch` exhausting its poll deadline.

Impact and severity

This is extremely annoying to unsuspecting new users.

High for operators who want to run strict SSRF policies.

The docs explicitly recommend hostnameAllowlist for strict deployments ("Strict-mode example" section in the browser docs), so any operator following that recommendation immediately breaks their managed browser. The bug is not a misuse of the configuration — it's a logical conflict between two documented features that can't currently coexist.

Additional information

Related: #64900 (WSL2 + Windows Edge remote CDP, same running: false / browser: unknown / attachOnly is enabled and profile ... is not running symptom pattern from a different platform). That report is tagged as a regression. The fact that the same failure mode reproduces on native Linux loopback with a local managed profile suggests the root cause is in the gateway-side reachability logic in createProfileAvailability, independent of network-bridge or remote-host specifics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingregressionBehavior that previously worked and now fails

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions