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:
- open an innocuous tab such as
about:blank
- install or interact with a page element whose
href points to http://127.0.0.1/...
- use
/snapshot to get a valid ref
- invoke
/act with kind=click
- 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
- Download the browser-control server runner from: browser_control_server_runner.mjs
- Download the shared harness from: browser_interaction_navigation_harness.py
- Download the control script from: control-direct-open-blocked.py
- Download the exploit script from: verification_test.py
- Run the control script:
python3 control-direct-open-blocked.py
- Confirm direct navigation to the loopback canary is rejected with
Blocked: private/internal IP address
- Run the exploit script:
python3 verification_test.py
- 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
- 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
Advisory Details
Title: Browser
/actclick interactions bypass the configured SSRF navigation policy and reach loopback targetsDescription:
Summary
openclaw-cncontains an SSRF-policy bypass in its browser control subsystem. Direct browser navigations are checked againstbrowser.ssrfPolicy, but the public/actinteraction 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:
assertBrowserNavigationAllowedclickinteraction path does not validate the destination after the click completesThe 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:
navigateViaPlaywrightultimately enforces:Tab creation goes through the same guard:
By contrast, the
/actclick path only dispatches the request and executes the click:And
clickViaPlaywrightitself simply performs the Playwright action:There is no post-click verification of the resulting
page.url(). As a result, an attacker can:about:blankhrefpoints tohttp://127.0.0.1/.../snapshotto get a validref/actwithkind=clickThis 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
openclaw-cncorresponding to affected version0.2.1browser.enabled=truebrowser.headless=truebrowser.noSandbox=truebrowser.ssrfPolicy.allowPrivateNetwork=falseReproduction Steps
python3 control-direct-open-blocked.pyBlocked: private/internal IP addresspython3 verification_test.pyabout:blank/actwithkind=evaluateto install a link to the loopback canary/snapshotto resolve the link ref/actwithkind=clickto activate that refhttp://127.0.0.1:<port>/canary?...and the local canary hit count increments to1Log of Evidence
Control-path evidence:
Exploit-path evidence:
Impact
This is an SSRF-policy bypass in the managed browser subsystem.
Impacted users are deployments that rely on
browser.ssrfPolicyto 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:
127.0.0.1openclaw-cnThe 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
Severity
Weaknesses
Occurrences
/acthandler dispatchesclickrequests toclickViaPlaywrightand returns success without checking whether the click caused a navigation to a blocked destination.clickViaPlaywrightexecuteslocator.click()but performs no post-click validation of the resultingpage.url(), allowing interaction-triggered navigations to bypass the SSRF policy./navigatepath explicitly passesssrfPolicyintonavigateViaPlaywright, showing the intended protected behavior for direct navigations.openTabenforcesassertBrowserNavigationAllowedbefore creating a tab, demonstrating that direct tab creation is guarded while the interaction path is not.httpandhttpsnavigation destinations viaresolvePinnedHostnameWithPolicy, but this guard is only effective where it is explicitly invoked.