diff --git a/tools/browser_tool.py b/tools/browser_tool.py index ffb772c1d..b58f388c1 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -170,6 +170,9 @@ def _resolve_cdp_override(cdp_url: str) -> str: For discovery-style endpoints we fetch /json/version and return the webSocketDebuggerUrl so downstream tools always receive a concrete browser websocket instead of an ambiguous host:port URL. + + SECURITY FIX (V-010): Validates URLs before fetching to prevent SSRF. + Only allows localhost/private network addresses for CDP connections. """ raw = (cdp_url or "").strip() if not raw: @@ -191,6 +194,35 @@ def _resolve_cdp_override(cdp_url: str) -> str: else: version_url = discovery_url.rstrip("/") + "/json/version" + # SECURITY FIX (V-010): Validate URL before fetching + # Only allow localhost and private networks for CDP + from urllib.parse import urlparse + parsed = urlparse(version_url) + hostname = parsed.hostname or "" + + # Allow only safe hostnames for CDP + allowed_hostnames = ["localhost", "127.0.0.1", "0.0.0.0", "::1"] + if hostname not in allowed_hostnames: + # Check if it's a private IP + try: + import ipaddress + ip = ipaddress.ip_address(hostname) + if not (ip.is_private or ip.is_loopback): + logger.error( + "SECURITY: Rejecting CDP URL '%s' - only localhost and private " + "networks are allowed to prevent SSRF attacks.", + raw + ) + return raw # Return original without fetching + except ValueError: + # Not an IP - reject unknown hostnames + logger.error( + "SECURITY: Rejecting CDP URL '%s' - unknown hostname '%s'. " + "Only localhost and private IPs are allowed.", + raw, hostname + ) + return raw + try: response = requests.get(version_url, timeout=10) response.raise_for_status()