fix: pin validated dns for ssrf checks
This commit is contained in:
@@ -11,6 +11,8 @@ import pytest
|
||||
from nanobot.security.network import (
|
||||
configure_ssrf_whitelist,
|
||||
contains_internal_url,
|
||||
pin_resolved_url_dns,
|
||||
resolve_url_target,
|
||||
validate_url_target,
|
||||
)
|
||||
|
||||
@@ -157,6 +159,25 @@ def test_allows_public_ip():
|
||||
assert ok, f"Should allow public IP, got: {err}"
|
||||
|
||||
|
||||
def test_resolve_url_target_returns_validated_public_ips():
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("example.com", ["93.184.216.34"])):
|
||||
ok, err, resolved_ips = resolve_url_target("http://example.com/page")
|
||||
|
||||
assert ok, err
|
||||
assert resolved_ips == ("93.184.216.34",)
|
||||
|
||||
|
||||
def test_pin_resolved_url_dns_prevents_second_resolution_rebind():
|
||||
def _rebinding_resolver(hostname, port, family=0, type_=0):
|
||||
return [(socket.AF_INET, socket.SOCK_STREAM, 0, "", ("169.254.169.254", 0))]
|
||||
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _rebinding_resolver):
|
||||
with pin_resolved_url_dns("http://example.com/page", ("93.184.216.34",)):
|
||||
infos = socket.getaddrinfo("example.com", 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
|
||||
assert infos[0][4][0] == "93.184.216.34"
|
||||
|
||||
|
||||
def test_allows_normal_https():
|
||||
with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve("github.com", ["140.82.121.3"])):
|
||||
ok, err = validate_url_target("https://github.com/HKUDS/nanobot")
|
||||
|
||||
@@ -804,6 +804,7 @@ async def test_connect_mcp_servers_http_clients_reject_unsafe_redirect_targets(
|
||||
|
||||
monkeypatch.setattr(mcp_mod, "validate_url_target", _validate)
|
||||
monkeypatch.setattr(mcp_mod, "_probe_http_url", _reachable)
|
||||
monkeypatch.setattr(mcp_mod, "PinnedDNSAsyncTransport", lambda: httpx.MockTransport(_handler))
|
||||
monkeypatch.setattr(mcp_mod.httpx, "AsyncClient", _async_client_with_mock_transport)
|
||||
monkeypatch.setattr(sys.modules["mcp.client.sse"], "sse_client", _fake_sse_client)
|
||||
monkeypatch.setattr(
|
||||
|
||||
@@ -10,7 +10,7 @@ import httpx
|
||||
import pytest
|
||||
|
||||
from nanobot.agent.tools import web as web_module
|
||||
from nanobot.agent.tools.web import WebFetchTool
|
||||
from nanobot.agent.tools.web import WebFetchTool, _get_with_safe_redirects
|
||||
from nanobot.config.schema import WebFetchConfig
|
||||
from nanobot.security.workspace_access import (
|
||||
bind_workspace_scope,
|
||||
@@ -97,6 +97,30 @@ async def test_web_fetch_result_contains_untrusted_flag():
|
||||
assert "[External content" in data.get("text", "")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_safe_redirect_request_pins_validated_dns(monkeypatch):
|
||||
calls: list[str] = []
|
||||
|
||||
def _rebinding_resolver(hostname, port, family=0, type_=0):
|
||||
calls.append(hostname)
|
||||
ip = "93.184.216.34" if len(calls) == 1 else "169.254.169.254"
|
||||
return [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (ip, 0))]
|
||||
|
||||
class FakeClient:
|
||||
async def get(self, url, headers=None, follow_redirects=False):
|
||||
infos = socket.getaddrinfo("attacker.example", 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
assert infos[0][4][0] == "93.184.216.34"
|
||||
return httpx.Response(200, request=httpx.Request("GET", url))
|
||||
|
||||
monkeypatch.setattr("nanobot.security.network.socket.getaddrinfo", _rebinding_resolver)
|
||||
|
||||
response, error = await _get_with_safe_redirects(FakeClient(), "https://attacker.example/")
|
||||
|
||||
assert error is None
|
||||
assert response is not None
|
||||
assert calls == ["attacker.example"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_web_fetch_can_skip_jina_and_use_custom_user_agent(monkeypatch):
|
||||
tool = WebFetchTool(
|
||||
|
||||
Reference in New Issue
Block a user