diff --git a/tests/tools/test_browser_url_validation.py b/tests/tools/test_browser_url_validation.py new file mode 100644 index 0000000000000..18e1a182e9212 --- /dev/null +++ b/tests/tools/test_browser_url_validation.py @@ -0,0 +1,48 @@ +"""Tests for browser_navigate URL scheme and SSRF validation.""" + +import json +from unittest.mock import patch + +from tools.browser_tool import browser_navigate + + +class TestBrowserUrlSchemeValidation: + """browser_navigate must reject non-http(s) schemes.""" + + def test_file_scheme_blocked(self): + result = json.loads(browser_navigate("file:///etc/shadow")) + assert result["success"] is False + assert "scheme" in result["error"].lower() + + def test_javascript_scheme_blocked(self): + result = json.loads(browser_navigate("javascript:alert(1)")) + assert result["success"] is False + assert "scheme" in result["error"].lower() + + def test_data_scheme_blocked(self): + result = json.loads(browser_navigate("data:text/html,

pwned

")) + assert result["success"] is False + assert "scheme" in result["error"].lower() + + def test_ftp_scheme_blocked(self): + result = json.loads(browser_navigate("ftp://evil.com/malware")) + assert result["success"] is False + assert "scheme" in result["error"].lower() + + def test_http_scheme_allowed(self): + """http URLs should pass scheme check (may fail later on browser connect).""" + with patch("tools.browser_tool._run_browser_command", + return_value={"success": True, "data": {"title": "Test", "url": "http://example.com"}}): + with patch("tools.browser_tool.check_website_access", return_value=None): + with patch("tools.browser_tool._get_session_info", return_value={"_first_nav": False}): + result = json.loads(browser_navigate("http://example.com")) + assert result.get("success") is True or "error" not in result or "scheme" not in result.get("error", "") + + def test_https_scheme_allowed(self): + """https URLs should pass scheme check.""" + with patch("tools.browser_tool._run_browser_command", + return_value={"success": True, "data": {"title": "Test", "url": "https://example.com"}}): + with patch("tools.browser_tool.check_website_access", return_value=None): + with patch("tools.browser_tool._get_session_info", return_value={"_first_nav": False}): + result = json.loads(browser_navigate("https://example.com")) + assert "scheme" not in result.get("error", "") diff --git a/tests/tools/test_website_policy.py b/tests/tools/test_website_policy.py index 52618a1d63cb2..6aaec73e7a6c3 100644 --- a/tests/tools/test_website_policy.py +++ b/tests/tools/test_website_policy.py @@ -292,6 +292,8 @@ def test_check_website_access_blocks_scheme_less_urls(tmp_path): def test_browser_navigate_returns_policy_block(monkeypatch): from tools import browser_tool + # Allow the URL past SSRF check so the website policy check is reached + monkeypatch.setattr(browser_tool, "_is_browser_url_safe", lambda url: True) monkeypatch.setattr( browser_tool, "check_website_access", diff --git a/tools/browser_tool.py b/tools/browser_tool.py index 5aea75f552095..22f268c795247 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -70,6 +70,11 @@ from tools.website_policy import check_website_access except Exception: check_website_access = lambda url: None # noqa: E731 — fail-open if policy module unavailable + +try: + from tools.url_safety import is_safe_url as _is_browser_url_safe +except Exception: + _is_browser_url_safe = lambda url: True # noqa: E731 — fail-open if module unavailable from tools.browser_providers.base import CloudBrowserProvider from tools.browser_providers.browserbase import BrowserbaseProvider from tools.browser_providers.browser_use import BrowserUseProvider @@ -947,6 +952,22 @@ def browser_navigate(url: str, task_id: Optional[str] = None) -> str: Returns: JSON string with navigation result (includes stealth features info on first nav) """ + # Restrict to http/https — block file://, javascript:, data:, etc. + from urllib.parse import urlparse as _urlparse + _parsed = _urlparse(url) + if _parsed.scheme not in ("http", "https", ""): + return json.dumps({ + "success": False, + "error": f"Unsupported URL scheme '{_parsed.scheme}'. Only http and https are allowed.", + }) + + # SSRF protection — block private/internal addresses + if not _is_browser_url_safe(url): + return json.dumps({ + "success": False, + "error": "Blocked: URL targets a private or internal network address.", + }) + # Website policy check — block before navigating blocked = check_website_access(url) if blocked: