diff --git a/tests/tools/test_browser_ssrf_local.py b/tests/tools/test_browser_ssrf_local.py index 27b6e3933b..b3b8bd2271 100644 --- a/tests/tools/test_browser_ssrf_local.py +++ b/tests/tools/test_browser_ssrf_local.py @@ -235,3 +235,21 @@ class TestPostRedirectSsrf: assert result["success"] is True assert result["url"] == final + + +class TestAllowPrivateUrlsConfig: + @pytest.fixture(autouse=True) + def _reset_cache(self): + browser_tool._allow_private_urls_resolved = False + browser_tool._cached_allow_private_urls = None + yield + browser_tool._allow_private_urls_resolved = False + browser_tool._cached_allow_private_urls = None + + def test_browser_config_string_false_stays_disabled(self, monkeypatch): + monkeypatch.setattr( + "hermes_cli.config.read_raw_config", + lambda: {"browser": {"allow_private_urls": "false"}}, + ) + + assert browser_tool._allow_private_urls() is False diff --git a/tests/tools/test_url_safety.py b/tests/tools/test_url_safety.py index 9377fc40e0..12b5b92ac5 100644 --- a/tests/tools/test_url_safety.py +++ b/tests/tools/test_url_safety.py @@ -259,6 +259,20 @@ class TestGlobalAllowPrivateUrls: with patch("hermes_cli.config.read_raw_config", return_value=cfg): assert _global_allow_private_urls() is True + def test_config_security_string_false_stays_disabled(self, monkeypatch): + """Quoted false must not opt out of SSRF protection.""" + monkeypatch.delenv("HERMES_ALLOW_PRIVATE_URLS", raising=False) + cfg = {"security": {"allow_private_urls": "false"}} + with patch("hermes_cli.config.read_raw_config", return_value=cfg): + assert _global_allow_private_urls() is False + + def test_config_browser_string_false_stays_disabled(self, monkeypatch): + """Legacy browser.allow_private_urls also normalises quoted false.""" + monkeypatch.delenv("HERMES_ALLOW_PRIVATE_URLS", raising=False) + cfg = {"browser": {"allow_private_urls": "false"}} + with patch("hermes_cli.config.read_raw_config", return_value=cfg): + assert _global_allow_private_urls() is False + def test_config_security_takes_precedence_over_browser(self, monkeypatch): """security section is checked before browser section.""" monkeypatch.delenv("HERMES_ALLOW_PRIVATE_URLS", raising=False) diff --git a/tools/browser_tool.py b/tools/browser_tool.py index aecb2ee7f6..3fde1dd9c6 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -67,6 +67,7 @@ from typing import Dict, Any, Optional, List, Tuple from pathlib import Path from agent.auxiliary_client import call_llm from hermes_constants import get_hermes_home +from utils import is_truthy_value try: from tools.website_policy import check_website_access @@ -639,7 +640,11 @@ def _allow_private_urls() -> bool: try: from hermes_cli.config import read_raw_config cfg = read_raw_config() - _cached_allow_private_urls = bool(cfg.get("browser", {}).get("allow_private_urls")) + browser_cfg = cfg.get("browser", {}) + if isinstance(browser_cfg, dict): + _cached_allow_private_urls = is_truthy_value( + browser_cfg.get("allow_private_urls"), default=False + ) except Exception as e: logger.debug("Could not read allow_private_urls from config: %s", e) return _cached_allow_private_urls diff --git a/tools/url_safety.py b/tools/url_safety.py index 7ff09ebb50..860d4d9dfa 100644 --- a/tools/url_safety.py +++ b/tools/url_safety.py @@ -29,6 +29,8 @@ import os import socket from urllib.parse import urlparse +from utils import is_truthy_value + logger = logging.getLogger(__name__) # Hostnames that should always be blocked regardless of IP resolution @@ -107,12 +109,16 @@ def _global_allow_private_urls() -> bool: cfg = read_raw_config() # security.allow_private_urls (preferred) sec = cfg.get("security", {}) - if isinstance(sec, dict) and sec.get("allow_private_urls"): + if isinstance(sec, dict) and is_truthy_value( + sec.get("allow_private_urls"), default=False + ): _cached_allow_private = True return _cached_allow_private # browser.allow_private_urls (legacy fallback) browser = cfg.get("browser", {}) - if isinstance(browser, dict) and browser.get("allow_private_urls"): + if isinstance(browser, dict) and is_truthy_value( + browser.get("allow_private_urls"), default=False + ): _cached_allow_private = True return _cached_allow_private except Exception: