mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-05 10:52:36 +00:00
fix(security): add ssrfWhitelist config to unblock Tailscale/CGNAT (#2669)
This commit is contained in:
parent
193eccdac7
commit
5f08d61d8f
@ -37,17 +37,27 @@ def load_config(config_path: Path | None = None) -> Config:
|
||||
"""
|
||||
path = config_path or get_config_path()
|
||||
|
||||
config = Config()
|
||||
if path.exists():
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data = _migrate_config(data)
|
||||
return Config.model_validate(data)
|
||||
config = Config.model_validate(data)
|
||||
except (json.JSONDecodeError, ValueError, pydantic.ValidationError) as e:
|
||||
logger.warning(f"Failed to load config from {path}: {e}")
|
||||
logger.warning("Using default configuration.")
|
||||
|
||||
return Config()
|
||||
_apply_ssrf_whitelist(config)
|
||||
return config
|
||||
|
||||
|
||||
def _apply_ssrf_whitelist(config: Config) -> None:
|
||||
"""Apply SSRF whitelist from config to the network security module."""
|
||||
if config.tools.ssrf_whitelist:
|
||||
from nanobot.security.network import configure_ssrf_whitelist
|
||||
|
||||
configure_ssrf_whitelist(config.tools.ssrf_whitelist)
|
||||
|
||||
|
||||
def save_config(config: Config, config_path: Path | None = None) -> None:
|
||||
|
||||
@ -192,6 +192,7 @@ class ToolsConfig(Base):
|
||||
exec: ExecToolConfig = Field(default_factory=ExecToolConfig)
|
||||
restrict_to_workspace: bool = False # If true, restrict all tool access to workspace directory
|
||||
mcp_servers: dict[str, MCPServerConfig] = Field(default_factory=dict)
|
||||
ssrf_whitelist: list[str] = Field(default_factory=list) # CIDR ranges to exempt from SSRF blocking (e.g. ["100.64.0.0/10"] for Tailscale)
|
||||
|
||||
|
||||
class Config(BaseSettings):
|
||||
|
||||
@ -22,8 +22,24 @@ _BLOCKED_NETWORKS = [
|
||||
|
||||
_URL_RE = re.compile(r"https?://[^\s\"'`;|<>]+", re.IGNORECASE)
|
||||
|
||||
_allowed_networks: list[ipaddress.IPv4Network | ipaddress.IPv6Network] = []
|
||||
|
||||
|
||||
def configure_ssrf_whitelist(cidrs: list[str]) -> None:
|
||||
"""Allow specific CIDR ranges to bypass SSRF blocking (e.g. Tailscale's 100.64.0.0/10)."""
|
||||
global _allowed_networks
|
||||
nets = []
|
||||
for cidr in cidrs:
|
||||
try:
|
||||
nets.append(ipaddress.ip_network(cidr, strict=False))
|
||||
except ValueError:
|
||||
pass
|
||||
_allowed_networks = nets
|
||||
|
||||
|
||||
def _is_private(addr: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
||||
if _allowed_networks and any(addr in net for net in _allowed_networks):
|
||||
return False
|
||||
return any(addr in net for net in _BLOCKED_NETWORKS)
|
||||
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from nanobot.security.network import contains_internal_url, validate_url_target
|
||||
from nanobot.security.network import configure_ssrf_whitelist, contains_internal_url, validate_url_target
|
||||
|
||||
|
||||
def _fake_resolve(host: str, results: list[str]):
|
||||
@ -99,3 +99,47 @@ def test_allows_normal_curl():
|
||||
|
||||
def test_no_urls_returns_false():
|
||||
assert not contains_internal_url("echo hello && ls -la")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSRF whitelist — allow specific CIDR ranges (#2669)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_blocks_cgnat_by_default():
|
||||
"""100.64.0.0/10 (CGNAT / Tailscale) is blocked by default."""
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("ts.local", ["100.100.1.1"])):
|
||||
ok, _ = validate_url_target("http://ts.local/api")
|
||||
assert not ok
|
||||
|
||||
|
||||
def test_whitelist_allows_cgnat():
|
||||
"""Whitelisting 100.64.0.0/10 lets Tailscale addresses through."""
|
||||
configure_ssrf_whitelist(["100.64.0.0/10"])
|
||||
try:
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("ts.local", ["100.100.1.1"])):
|
||||
ok, err = validate_url_target("http://ts.local/api")
|
||||
assert ok, f"Whitelisted CGNAT should be allowed, got: {err}"
|
||||
finally:
|
||||
configure_ssrf_whitelist([])
|
||||
|
||||
|
||||
def test_whitelist_does_not_affect_other_blocked():
|
||||
"""Whitelisting CGNAT must not unblock other private ranges."""
|
||||
configure_ssrf_whitelist(["100.64.0.0/10"])
|
||||
try:
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("evil.com", ["10.0.0.1"])):
|
||||
ok, _ = validate_url_target("http://evil.com/secret")
|
||||
assert not ok
|
||||
finally:
|
||||
configure_ssrf_whitelist([])
|
||||
|
||||
|
||||
def test_whitelist_invalid_cidr_ignored():
|
||||
"""Invalid CIDR entries are silently skipped."""
|
||||
configure_ssrf_whitelist(["not-a-cidr", "100.64.0.0/10"])
|
||||
try:
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("ts.local", ["100.100.1.1"])):
|
||||
ok, _ = validate_url_target("http://ts.local/api")
|
||||
assert ok
|
||||
finally:
|
||||
configure_ssrf_whitelist([])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user