fix(browser): raw CDP applies the metadata floor in both guard branches; docs state what it covers

The cloud-metadata check on raw CDP ran only with the private-address
guard off, and only on a `url` param. With the guard on, an unlisted
method such as Network.loadNetworkResource fetched a metadata host or a
loopback URL from a public page, because only the page URL was checked.

Every CDP call now screens each `url` param and every http(s) literal
in its string params (function bodies, injected scripts, HTML) against
the metadata floor whatever the guard says. With the guard on, unlisted
methods also refuse private literals in their own params.

browser_exec still follows a page's own redirect or fetch to a metadata
address: the floor reads the URLs the agent writes, not the browser's
later traffic. security.md and browser.md now say that instead of
"refused on every route", and point at network-level IMDS blocking.
This commit is contained in:
Teknium committed 2026-10-28 19:07:59 -07:00
1 parent 1fdfdecbcf
commit f4aa77a467
5 files changed
+71 -16

No files matched your search

+22
View File
@@ -101,3 +101,25 @@ def test_raw_cdp_keeps_the_metadata_floor_when_the_private_guard_is_off(monkeypa
assert "cloud metadata" in guard(method="Runtime.evaluate", params=metadata)
lan = {"expression": template.format("http://192.168.1.5/")}
assert guard(method="Runtime.evaluate", params=lan) is None
@pytest.mark.parametrize("local_backend", [False, True])
def test_raw_cdp_url_params_are_screened_even_when_the_open_page_is_public(monkeypatch, local_backend):
"""A public page does not vouch for a call's own URLs: unlisted resource-loading methods keep the metadata
floor with the private guard on or off, and the private-address check whenever the guard is on."""
from tools import browser_cdp_tool, browser_session_cloud
monkeypatch.setattr(browser_session_cloud, "is_local_backend", lambda: local_backend)
monkeypatch.setattr(browser_session_cloud, "allow_private_urls", lambda: False)
monkeypatch.setattr(browser_cdp_tool, "_current_page_private_url", lambda endpoint, target_id: None)
guard = browser_cdp_tool._browser_cdp_private_guard
metadata = "http://metadata.google.internal/computeMetadata/v1/"
calls = (("Network.loadNetworkResource", {"frameId": "F", "url": metadata, "options": {}}),
("Runtime.callFunctionOn", {"functionDeclaration": f"() => fetch('{metadata}')"}),
("Page.addScriptToEvaluateOnNewDocument", {"source": f"fetch(`{metadata}`)"}))
for method, params in calls:
assert "cloud metadata" in guard(method=method, params=params, endpoint="ws://x", target_id="T")
loopback = {"frameId": "F", "url": "http://127.0.0.1:8080/", "options": {}}
verdict = guard(method="Network.loadNetworkResource", params=loopback, endpoint="ws://x", target_id="T")
assert (verdict is None) == local_backend
public = {"frameId": "F", "url": "https://93.184.216.34/", "options": {}}
assert guard(method="Network.loadNetworkResource", params=public, endpoint="ws://x", target_id="T") is None
+32 -10
View File
@@ -197,6 +197,20 @@ def _current_page_private_url(endpoint: str, target_id: Optional[str]) -> Option
return next((u for u in page_urls if u and not u.startswith(_ORIGINLESS_PAGE_PREFIXES) and url_blocked(u)), None)
def _param_url_literals(value: Any, key: str = "") -> list[str]:
"""Every URL a CDP call's params name: each ``url`` value whole, plus the ``http(s)://`` literals in
every string at any depth (``Network.loadNetworkResource``, ``Runtime.callFunctionOn`` bodies,
``Page.addScriptToEvaluateOnNewDocument`` sources, injected HTML)."""
from tools.browser_url_policy import url_literals
if isinstance(value, str):
return ([value.strip()] if key == "url" and value.strip() else []) + url_literals(value)
if isinstance(value, dict):
return [u for k, v in value.items() for u in _param_url_literals(v, str(k))]
if isinstance(value, (list, tuple)):
return [u for v in value for u in _param_url_literals(v, key)]
return []
def _browser_cdp_private_guard(*, method: str, params: dict[str, Any], endpoint: str = "",
target_id: Optional[str] = None) -> Optional[str]:
"""Apply the browser SSRF/private-page guard to raw CDP calls.
@@ -205,21 +219,29 @@ def _browser_cdp_private_guard(*, method: str, params: dict[str, Any], endpoint:
its bypass, so a page whose URL cannot be verified as public refuses page-reading methods.
"""
from tools.browser_url_policy import eval_ssrf_guard_active, url_blocked
from tools.url_safety import is_always_blocked_url
guard_active = eval_ssrf_guard_active()
guard = _METHOD_PARAM_GUARDS.get(method)
if not eval_ssrf_guard_active():
# The cloud-metadata floor stays unconditional, as in browser_exec's _url_policy_error (#16234):
# every guarded method's probe, plus the ``url`` param any other method takes
# (Network.loadNetworkResource, Fetch.continueRequest).
from tools.url_safety import is_always_blocked_url
probe = guard[0] if guard is not None else _navigate_private_target
literal = probe(params or {}, is_always_blocked_url)
return _blocked(f"Blocked: CDP {method} targets a cloud metadata endpoint ({literal}).",
method) if literal else None
if guard is not None:
if guard_active and guard is not None:
probe, template = guard
literal = probe(params or {}, url_blocked)
if literal:
return _blocked(template.format(literal), method)
# The cloud-metadata floor is unconditional for every method, as in browser_exec's _url_policy_error
# (#16234): with the private guard on or off, sidecar and local Chromium included.
literals = _param_url_literals(params or {})
metadata = next((u for u in literals if is_always_blocked_url(u)), None)
if metadata:
return _blocked(f"Blocked: CDP {method} targets a cloud metadata endpoint ({metadata}).", method)
if not guard_active:
return None
if guard is None:
# A public page does not make an unlisted method's own URL params safe: Network.loadNetworkResource
# fetches its ``url`` from the browser's network position whatever page is open.
literal = next((u for u in literals if u.lower().startswith(("http://", "https://")) and url_blocked(u)), None)
if literal:
return _blocked(f"Blocked: CDP {method} params target a private or internal address ({literal}).",
method)
if method not in _CDP_PRIVATE_PAGE_ALLOWED_METHODS:
blocked_url = _current_page_private_url(endpoint, target_id)
if blocked_url == _PAGE_URL_UNVERIFIED:
+6 -2
View File
@@ -136,7 +136,11 @@ def url_blocked(url: str) -> bool:
return is_always_blocked_url(url) or not is_safe_url(url)
def url_literals(text: str) -> list[str]:
"""Every ``http(s)://`` literal in free text (a JS expression, an HTML snippet, a CDP param)."""
return [m.rstrip(".,;") for m in _JS_URL_LITERAL_RE.findall(text)] if isinstance(text, str) else []
def expression_targets_private_url(expression: str, *, blocked: Callable[[str], bool] = url_blocked) -> Optional[str]:
"""The first ``http(s)://`` literal in a JS expression that ``blocked`` refuses (best-effort), else None."""
literals = _JS_URL_LITERAL_RE.findall(expression) if isinstance(expression, str) else []
return next((c for c in (m.rstrip(".,;") for m in literals) if blocked(c)), None)
return next((c for c in url_literals(expression) if blocked(c)), None)
+6 -3
View File
@@ -113,7 +113,8 @@ and it works on headless servers with no desktop browser at all.
`security.allow_private_urls` says: it reaches nothing the terminal can't.
The private-address guard applies when the browser can reach networks the
terminal can't (a cloud provider, a `/browser connect` endpoint, or a
Docker/SSH/Modal terminal backend). Cloud-metadata endpoints are always refused.
Docker/SSH/Modal terminal backend). Cloud-metadata URLs the agent writes are
always refused.
**Which Chromium.** Hermes picks the first that exists:
@@ -282,8 +283,10 @@ With auto-routing disabled, private URLs are rejected with
`"Blocked: URL targets a private or internal address"` unless you also set
`security.allow_private_urls: true` (or the legacy `browser.allow_private_urls: true`),
which hands them to the cloud provider — usually useless, since Browserbase etc.
can't reach your LAN. Cloud-metadata endpoints (`169.254.169.254`,
`metadata.google.internal`) are refused on every route, sidecar included.
can't reach your LAN. Cloud-metadata URLs (`169.254.169.254`,
`metadata.google.internal`) written in `browser_exec` code or raw CDP params are refused on
every route, sidecar included; a page that redirects there on its own is not (see
[Security](../security.md#what-allow_private_urls-covers-and-what-it-doesnt)).
The sidecar is the same local Chromium described [above](#local-chromium-default),
so it needs that Chromium installed. Routing is decided from the URLs written in
+5 -1
View File
@@ -809,7 +809,11 @@ not stop every route to a private host. With it `false`:
| `terminal` (`curl http://localhost`, ...) | **Opens**: no URL guard |
Cloud-metadata endpoints are refused on every row except `terminal`, sidecar and
local Chromium included. To keep the agent's browser off private hosts while a cloud provider is
local Chromium included: a metadata URL written in `browser_exec` code, in a raw CDP call's
params, or in a script it injects is refused before the browser sees it. The check reads the URLs
the agent writes; it does not watch the browser afterwards, so a public page that redirects to, or
fetches from, a metadata address is not stopped by it. On a cloud VM, block the metadata service at
the network too (IMDSv2 with hop limit 1, a firewall rule). To keep the agent's browser off private hosts while a cloud provider is
configured, set `browser.auto_local_for_private_urls: false`; that matters most on
hosted installs (Docker, a VPS), where the sidecar sees the host's own loopback
services and its LAN.