Summary
Commit 42c076d3 (feat(browser): auto-spawn local Chromium for LAN/localhost URLs in cloud mode) introduces a hybrid browser routing feature that unintentionally bypasses the pre-navigation SSRF safety check for the entire 169.254.0.0/16 link-local range, including cloud instance metadata endpoints (169.254.169.254).
Affected configuration
- Cloud browser provider configured (Browserbase / Browser-Use / Firecrawl) — the default new-user path
browser.auto_local_for_private_urls: true (introduced default: True)
Both conditions are on by default, making this active for most cloud deployments out of the box.
Root cause
In tools/browser_tool.py, the pre-navigation SSRF guard was restructured to short-circuit when auto_local_this_nav is true:
if (
not _is_local_backend()
and not auto_local_this_nav # <-- bypass condition added in 42c076d3
and not _allow_private_urls()
and not _is_safe_url(url)
):
return json.dumps({"success": False, "error": "Blocked: ..."})
auto_local_this_nav is set to True whenever _url_is_private() classifies the URL as a private/loopback/LAN address. For 169.254.169.254, ipaddress.ip_address(...).is_link_local returns True, so _url_is_private() → True → auto_local_this_nav = True → _is_safe_url() is never called.
The result: browser_navigate routes the request to a locally-spawned Chromium sidecar, which can reach http://169.254.169.254/latest/meta-data/ and similar IMDS endpoints. The agent can then read the full response via browser_snapshot, exfiltrating AWS/GCP/Azure instance credentials from within a cloud deployment.
The two classifiers diverge
_url_is_private() (new, in browser_tool.py) and is_safe_url() (pre-existing, in url_safety.py) check different things:
| Check |
_url_is_private() |
is_safe_url() |
169.254.0.0/16 link-local |
✅ caught (is_link_local) |
✅ caught (_ALWAYS_BLOCKED_NETWORKS) |
| DNS failure |
returns False (fall-through) |
returns False → blocks (fail-closed) |
100.64.0.0/10 CGNAT |
✅ caught |
❌ not checked |
is_reserved |
❌ not checked |
✅ checked |
is_multicast |
❌ not checked |
✅ checked |
For 169.254.169.254, both classifiers catch it — but the problem is that _url_is_private() matching causes is_safe_url() to be skipped entirely rather than running it as a second gate.
Secondary finding
_url_is_private() relies on ipaddress.ip_address(...).is_private for the 172.16.0.0/12 range. This property only covers that range in Python ≥ 3.11 (it was expanded in bpo-40791). On Python 3.10 runtimes, 172.16.x.x addresses pass through to the cloud provider undetected.
Recommended fix
The routing decision (local vs. cloud) and the safety check are orthogonal concerns and should not be collapsed into the same conditional. The fix is to always call _is_safe_url() regardless of routing destination — or at minimum, preserve the _ALWAYS_BLOCKED_NETWORKS / _ALWAYS_BLOCKED_IPS check before routing to the local sidecar:
# Always run the safety check before routing anywhere
if (
not _is_local_backend()
and not _allow_private_urls()
and not _is_safe_url(url)
):
return json.dumps({"success": False, "error": "Blocked: ..."})
# Routing decision is separate
auto_local_this_nav = _url_is_private(url) and _cfg_auto_local_for_private_urls()
Workaround
Set browser.auto_local_for_private_urls: false in config, or avoid configuring a cloud browser provider in environments where IMDS access would be harmful.
Discovered via automated daily security review of HEAD..origin/main.
Summary
Commit
42c076d3(feat(browser): auto-spawn local Chromium for LAN/localhost URLs in cloud mode) introduces a hybrid browser routing feature that unintentionally bypasses the pre-navigation SSRF safety check for the entire169.254.0.0/16link-local range, including cloud instance metadata endpoints (169.254.169.254).Affected configuration
browser.auto_local_for_private_urls: true(introduced default:True)Both conditions are on by default, making this active for most cloud deployments out of the box.
Root cause
In
tools/browser_tool.py, the pre-navigation SSRF guard was restructured to short-circuit whenauto_local_this_navis true:auto_local_this_navis set toTruewhenever_url_is_private()classifies the URL as a private/loopback/LAN address. For169.254.169.254,ipaddress.ip_address(...).is_link_localreturnsTrue, so_url_is_private()→True→auto_local_this_nav = True→_is_safe_url()is never called.The result:
browser_navigateroutes the request to a locally-spawned Chromium sidecar, which can reachhttp://169.254.169.254/latest/meta-data/and similar IMDS endpoints. The agent can then read the full response viabrowser_snapshot, exfiltrating AWS/GCP/Azure instance credentials from within a cloud deployment.The two classifiers diverge
_url_is_private()(new, inbrowser_tool.py) andis_safe_url()(pre-existing, inurl_safety.py) check different things:_url_is_private()is_safe_url()169.254.0.0/16link-localFalse(fall-through)False→ blocks (fail-closed)100.64.0.0/10CGNATis_reservedis_multicastFor
169.254.169.254, both classifiers catch it — but the problem is that_url_is_private()matching causesis_safe_url()to be skipped entirely rather than running it as a second gate.Secondary finding
_url_is_private()relies onipaddress.ip_address(...).is_privatefor the172.16.0.0/12range. This property only covers that range in Python ≥ 3.11 (it was expanded in bpo-40791). On Python 3.10 runtimes,172.16.x.xaddresses pass through to the cloud provider undetected.Recommended fix
The routing decision (local vs. cloud) and the safety check are orthogonal concerns and should not be collapsed into the same conditional. The fix is to always call
_is_safe_url()regardless of routing destination — or at minimum, preserve the_ALWAYS_BLOCKED_NETWORKS/_ALWAYS_BLOCKED_IPScheck before routing to the local sidecar:Workaround
Set
browser.auto_local_for_private_urls: falsein config, or avoid configuring a cloud browser provider in environments where IMDS access would be harmful.Discovered via automated daily security review of
HEAD..origin/main.