The SSRF protection added in #3041 blocks all private/internal addresses unconditionally in browser_navigate(). This prevents legitimate local use cases (localhost apps, LAN devices) when using Camofox or the built-in headless Chromium without a cloud provider. The check is only meaningful for cloud backends (Browserbase, BrowserUse) where the agent could reach internal resources on a remote machine. Local backends give the user full terminal and network access already — the SSRF check adds zero security value. Add _is_local_backend() helper that returns True when Camofox is active or no cloud provider is configured. Both the pre-navigation and post-redirect SSRF checks now skip when running locally. The browser.allow_private_urls config option remains available as an explicit opt-out for cloud mode.
238 lines
9.7 KiB
Python
238 lines
9.7 KiB
Python
"""Tests that browser_navigate SSRF checks respect local-backend mode and
|
|
the allow_private_urls setting.
|
|
|
|
Local backends (Camofox, headless Chromium without a cloud provider) skip
|
|
SSRF checks entirely — the agent already has full local-network access via
|
|
the terminal tool.
|
|
|
|
Cloud backends (Browserbase, BrowserUse) enforce SSRF by default. Users
|
|
can opt out for cloud mode via ``browser.allow_private_urls: true``.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from tools import browser_tool
|
|
|
|
|
|
def _make_browser_result(url="https://example.com"):
|
|
"""Return a mock successful browser command result."""
|
|
return {"success": True, "data": {"title": "OK", "url": url}}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pre-navigation SSRF check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPreNavigationSsrf:
|
|
PRIVATE_URL = "http://127.0.0.1:8080/dashboard"
|
|
|
|
@pytest.fixture()
|
|
def _common_patches(self, monkeypatch):
|
|
"""Shared patches for pre-navigation tests that pass the SSRF check."""
|
|
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "check_website_access", lambda url: None)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_get_session_info",
|
|
lambda task_id: {
|
|
"session_name": f"s_{task_id}",
|
|
"bb_session_id": None,
|
|
"cdp_url": None,
|
|
"features": {"local": True},
|
|
"_first_nav": False,
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_run_browser_command",
|
|
lambda *a, **kw: _make_browser_result(),
|
|
)
|
|
|
|
# -- Cloud mode: SSRF active -----------------------------------------------
|
|
|
|
def test_cloud_blocks_private_url_by_default(self, monkeypatch, _common_patches):
|
|
"""SSRF protection blocks private URLs in cloud mode."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: False)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PRIVATE_URL))
|
|
|
|
assert result["success"] is False
|
|
assert "private or internal address" in result["error"]
|
|
|
|
def test_cloud_allows_private_url_when_setting_true(self, monkeypatch, _common_patches):
|
|
"""Private URLs pass in cloud mode when allow_private_urls is True."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: True)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: False)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PRIVATE_URL))
|
|
|
|
assert result["success"] is True
|
|
|
|
def test_cloud_allows_public_url(self, monkeypatch, _common_patches):
|
|
"""Public URLs always pass in cloud mode."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: True)
|
|
|
|
result = json.loads(browser_tool.browser_navigate("https://example.com"))
|
|
|
|
assert result["success"] is True
|
|
|
|
# -- Local mode: SSRF skipped ----------------------------------------------
|
|
|
|
def test_local_allows_private_url(self, monkeypatch, _common_patches):
|
|
"""Local backends skip SSRF — private URLs are always allowed."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: True)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: False)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PRIVATE_URL))
|
|
|
|
assert result["success"] is True
|
|
|
|
def test_local_allows_public_url(self, monkeypatch, _common_patches):
|
|
"""Local backends pass public URLs too (sanity check)."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: True)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: True)
|
|
|
|
result = json.loads(browser_tool.browser_navigate("https://example.com"))
|
|
|
|
assert result["success"] is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _is_local_backend() unit tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestIsLocalBackend:
|
|
def test_camofox_is_local(self, monkeypatch):
|
|
"""Camofox mode counts as a local backend."""
|
|
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: True)
|
|
monkeypatch.setattr(browser_tool, "_get_cloud_provider", lambda: "anything")
|
|
|
|
assert browser_tool._is_local_backend() is True
|
|
|
|
def test_no_cloud_provider_is_local(self, monkeypatch):
|
|
"""No cloud provider configured → local backend."""
|
|
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_get_cloud_provider", lambda: None)
|
|
|
|
assert browser_tool._is_local_backend() is True
|
|
|
|
def test_cloud_provider_is_not_local(self, monkeypatch):
|
|
"""Cloud provider configured and not Camofox → NOT local."""
|
|
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_get_cloud_provider", lambda: "bb")
|
|
|
|
assert browser_tool._is_local_backend() is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post-redirect SSRF check
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestPostRedirectSsrf:
|
|
PUBLIC_URL = "https://example.com/redirect"
|
|
PRIVATE_FINAL_URL = "http://192.168.1.1/internal"
|
|
|
|
@pytest.fixture()
|
|
def _common_patches(self, monkeypatch):
|
|
"""Shared patches for redirect tests."""
|
|
monkeypatch.setattr(browser_tool, "_is_camofox_mode", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "check_website_access", lambda url: None)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_get_session_info",
|
|
lambda task_id: {
|
|
"session_name": f"s_{task_id}",
|
|
"bb_session_id": None,
|
|
"cdp_url": None,
|
|
"features": {"local": True},
|
|
"_first_nav": False,
|
|
},
|
|
)
|
|
|
|
# -- Cloud mode: redirect SSRF active --------------------------------------
|
|
|
|
def test_cloud_blocks_redirect_to_private(self, monkeypatch, _common_patches):
|
|
"""Redirects to private addresses are blocked in cloud mode."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(
|
|
browser_tool, "_is_safe_url", lambda url: "192.168" not in url,
|
|
)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_run_browser_command",
|
|
lambda *a, **kw: _make_browser_result(url=self.PRIVATE_FINAL_URL),
|
|
)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PUBLIC_URL))
|
|
|
|
assert result["success"] is False
|
|
assert "redirect landed on a private/internal address" in result["error"]
|
|
|
|
def test_cloud_allows_redirect_to_private_when_setting_true(self, monkeypatch, _common_patches):
|
|
"""Redirects to private addresses pass in cloud mode with allow_private_urls."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: True)
|
|
monkeypatch.setattr(
|
|
browser_tool, "_is_safe_url", lambda url: "192.168" not in url,
|
|
)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_run_browser_command",
|
|
lambda *a, **kw: _make_browser_result(url=self.PRIVATE_FINAL_URL),
|
|
)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PUBLIC_URL))
|
|
|
|
assert result["success"] is True
|
|
assert result["url"] == self.PRIVATE_FINAL_URL
|
|
|
|
# -- Local mode: redirect SSRF skipped -------------------------------------
|
|
|
|
def test_local_allows_redirect_to_private(self, monkeypatch, _common_patches):
|
|
"""Redirects to private addresses pass in local mode."""
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: True)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(
|
|
browser_tool, "_is_safe_url", lambda url: "192.168" not in url,
|
|
)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_run_browser_command",
|
|
lambda *a, **kw: _make_browser_result(url=self.PRIVATE_FINAL_URL),
|
|
)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PUBLIC_URL))
|
|
|
|
assert result["success"] is True
|
|
assert result["url"] == self.PRIVATE_FINAL_URL
|
|
|
|
def test_cloud_allows_redirect_to_public(self, monkeypatch, _common_patches):
|
|
"""Redirects to public addresses always pass (cloud mode)."""
|
|
final = "https://example.com/final"
|
|
monkeypatch.setattr(browser_tool, "_is_local_backend", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_allow_private_urls", lambda: False)
|
|
monkeypatch.setattr(browser_tool, "_is_safe_url", lambda url: True)
|
|
monkeypatch.setattr(
|
|
browser_tool,
|
|
"_run_browser_command",
|
|
lambda *a, **kw: _make_browser_result(url=final),
|
|
)
|
|
|
|
result = json.loads(browser_tool.browser_navigate(self.PUBLIC_URL))
|
|
|
|
assert result["success"] is True
|
|
assert result["url"] == final
|