Skip to content

[Security] Browser /act click interactions bypass the configured SSRF navigation policy and reach loopback targets #602

Description

@YLChen-007

Advisory Details

Title: Browser /act click interactions bypass the configured SSRF navigation policy and reach loopback targets

Description:

Summary

openclaw-cn contains an SSRF-policy bypass in its browser control subsystem. Direct browser navigations are checked against browser.ssrfPolicy, but the public /act interaction path can still drive an existing tab to a loopback or private-network URL through a click-triggered navigation. An authenticated browser-control caller, or an agent/tool flow allowed to use browser interactions, can therefore reach targets that the product explicitly claims to block on direct navigation paths.

Details

The browser control service exposes a local HTTP API for browser automation. The intended safety boundary is clear in the implementation:

  • direct navigation routes validate the destination URL with assertBrowserNavigationAllowed
  • tab creation validates the URL with the same guard
  • the click interaction path does not validate the destination after the click completes

The root cause is that the SSRF guard is applied only before direct navigation, but not after user-triggered interactions that can indirectly navigate the tab.

The direct navigation path is protected here:

const ssrfPolicy = ctx.state().resolved.ssrfPolicy;
const result = await pw.navigateViaPlaywright({
  cdpUrl: profileCtx.profile.cdpUrl,
  targetId: tab.targetId,
  url,
  ...(ssrfPolicy ? { ssrfPolicy } : {}),
});

navigateViaPlaywright ultimately enforces:

await assertBrowserNavigationAllowed({
  url,
  ssrfPolicy: opts.ssrfPolicy,
});

Tab creation goes through the same guard:

const ssrfPolicy = state().resolved.ssrfPolicy;
await assertBrowserNavigationAllowed({ url, ssrfPolicy });

By contrast, the /act click path only dispatches the request and executes the click:

const clickRequest: Parameters<typeof pw.clickViaPlaywright>[0] = {
  cdpUrl,
  targetId: tab.targetId,
  ref,
  doubleClick,
};
await pw.clickViaPlaywright(clickRequest);
return res.json({ ok: true, targetId: tab.targetId, url: tab.url });

And clickViaPlaywright itself simply performs the Playwright action:

await locator.click({
  timeout,
  button: opts.button,
  modifiers: opts.modifiers,
});

There is no post-click verification of the resulting page.url(). As a result, an attacker can:

  1. open an innocuous tab such as about:blank
  2. install or interact with a page element whose href points to http://127.0.0.1/...
  3. use /snapshot to get a valid ref
  4. invoke /act with kind=click
  5. cause the managed browser to navigate to a loopback target that direct navigation would have blocked

This is a genuine security boundary failure because the product already advertises and implements SSRF blocking for direct navigations, yet the standard interaction surface bypasses that same restriction.

PoC

Prerequisites

  • A build or install of openclaw-cn corresponding to affected version 0.2.1
  • Node.js 22.12.0 or later
  • A local Chrome/Chromium executable available to the browser control subsystem
  • Browser control enabled with a policy equivalent to:
    • browser.enabled=true
    • browser.headless=true
    • browser.noSandbox=true
    • browser.ssrfPolicy.allowPrivateNetwork=false
  • Access to the browser control HTTP API as an authenticated caller

Reproduction Steps

  1. Download the browser-control server runner from: browser_control_server_runner.mjs
  2. Download the shared harness from: browser_interaction_navigation_harness.py
  3. Download the control script from: control-direct-open-blocked.py
  4. Download the exploit script from: verification_test.py
  5. Run the control script:
    python3 control-direct-open-blocked.py
  6. Confirm direct navigation to the loopback canary is rejected with Blocked: private/internal IP address
  7. Run the exploit script:
    python3 verification_test.py
  8. Observe that the script:
    • opens about:blank
    • uses /act with kind=evaluate to install a link to the loopback canary
    • uses /snapshot to resolve the link ref
    • uses /act with kind=click to activate that ref
  9. Confirm that the target tab ultimately lands on http://127.0.0.1:<port>/canary?... and the local canary hit count increments to 1

Log of Evidence

Control-path evidence:

/tabs/open private-target status=500 body={'error': 'SsrFBlockedError: Blocked: private/internal IP address'}
canary hits=0
Observation: direct private navigation was denied by the configured browser SSRF policy.

Exploit-path evidence:

/tabs/open about:blank status=200 body={'targetId': '374C2E4F3B091033D60D203EF187C035', 'title': 'about:blank', 'url': 'about:blank', ...}
/act evaluate-install-link status=200 body={'ok': True, 'targetId': '374C2E4F3B091033D60D203EF187C035', 'url': 'about:blank', 'result': {'ready': True}}
/snapshot status=200 body={'ok': True, 'format': 'ai', 'targetId': '374C2E4F3B091033D60D203EF187C035', 'url': 'about:blank', 'snapshot': '- link "go" [ref=e2] ...'}
/act click status=200 body={'ok': True, 'targetId': '374C2E4F3B091033D60D203EF187C035', 'url': 'about:blank'}
navigated tab: {'targetId': '374C2E4F3B091033D60D203EF187C035', 'title': '127.0.0.1:57089/canary?issue=Advisory-GHSA-vr5g-mmx7-h897-browser-interaction-navigation', 'url': 'http://127.0.0.1:57089/canary?issue=Advisory-GHSA-vr5g-mmx7-h897-browser-interaction-navigation', ...}
canary hits=1
Observation: the public HTTP /act click interface returned success, the tab URL changed to the SSRF-blocked loopback target, and the canary server recorded a request.

Impact

This is an SSRF-policy bypass in the managed browser subsystem.

Impacted users are deployments that rely on browser.ssrfPolicy to prevent the controlled browser from reaching loopback or private-network targets. An authenticated browser-control caller, or an accepted agent/tool flow with browser interaction capability, can use standard browser actions to reach services that the direct navigation path correctly blocks.

In practice this can expose:

  • local-only web services bound to 127.0.0.1
  • private-network HTTP endpoints reachable from the host running openclaw-cn
  • internal admin/debug panels or metadata-style targets that operators reasonably expected the SSRF policy to deny

The issue does not create unauthenticated internet-wide reach by itself, but it breaks a documented in-product protection boundary and turns the managed browser into a loopback/private-network pivot inside the authorized browser-control trust zone.

Affected products

  • Ecosystem: npm
  • Package name: openclaw-cn
  • Affected versions: <= 0.2.1
  • Patched versions:

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

Weaknesses

  • CWE: CWE-918: Server-Side Request Forgery (SSRF)

Occurrences

Permalink Description
https://github.com/jiulingyun/openclaw-cn/blob/558f272e6c90e7e0c37644e505e161b91ef738f0/src/browser/routes/agent.act.ts#L48-L80 The public /act handler dispatches click requests to clickViaPlaywright and returns success without checking whether the click caused a navigation to a blocked destination.
https://github.com/jiulingyun/openclaw-cn/blob/558f272e6c90e7e0c37644e505e161b91ef738f0/src/browser/pw-tools-core.interactions.ts#L34-L60 clickViaPlaywright executes locator.click() but performs no post-click validation of the resulting page.url(), allowing interaction-triggered navigations to bypass the SSRF policy.
https://github.com/jiulingyun/openclaw-cn/blob/558f272e6c90e7e0c37644e505e161b91ef738f0/src/browser/routes/agent.snapshot.ts#L48-L59 The direct /navigate path explicitly passes ssrfPolicy into navigateViaPlaywright, showing the intended protected behavior for direct navigations.
https://github.com/jiulingyun/openclaw-cn/blob/558f272e6c90e7e0c37644e505e161b91ef738f0/src/browser/server-context.ts#L151-L180 openTab enforces assertBrowserNavigationAllowed before creating a tab, demonstrating that direct tab creation is guarded while the interaction path is not.
https://github.com/jiulingyun/openclaw-cn/blob/558f272e6c90e7e0c37644e505e161b91ef738f0/src/browser/navigation-guard.ts#L1-L27 The SSRF policy implementation validates http and https navigation destinations via resolvePinnedHostnameWithPolicy, but this guard is only effective where it is explicitly invoked.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions