Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions tests/tools/test_browser_url_validation.py
Original file line number Diff line number Diff line change
@@ -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,<h1>pwned</h1>"))
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", "")
2 changes: 2 additions & 0 deletions tests/tools/test_website_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions tools/browser_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading