fix: reject proxied pinned web fetches
This commit is contained in:
@@ -25,7 +25,6 @@ from nanobot.bus.events import (
|
||||
)
|
||||
from nanobot.security.network import (
|
||||
PinnedDNSAsyncTransport,
|
||||
pin_resolved_url_dns,
|
||||
resolve_url_target,
|
||||
validate_url_target,
|
||||
)
|
||||
@@ -184,11 +183,11 @@ async def _probe_http_url(url: str, timeout: float = 3.0) -> bool:
|
||||
if not ok:
|
||||
return False
|
||||
try:
|
||||
with pin_resolved_url_dns(url, resolved_ips):
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(host, port),
|
||||
timeout=timeout,
|
||||
)
|
||||
target_host = resolved_ips[0] if resolved_ips else host
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(target_host, port),
|
||||
timeout=timeout,
|
||||
)
|
||||
writer.close()
|
||||
with suppress(OSError, asyncio.TimeoutError):
|
||||
await asyncio.wait_for(writer.wait_closed(), timeout=0.2)
|
||||
|
||||
@@ -118,10 +118,10 @@ def _resolve_url_safe(url: str) -> tuple[bool, str, tuple[str, ...]]:
|
||||
return resolve_url_target(url)
|
||||
|
||||
|
||||
def _pinned_dns_transport(proxy: str | None = None) -> httpx.AsyncBaseTransport:
|
||||
def _pinned_dns_transport() -> httpx.AsyncBaseTransport:
|
||||
from nanobot.security.network import PinnedDNSAsyncTransport
|
||||
|
||||
return PinnedDNSAsyncTransport(proxy=proxy)
|
||||
return PinnedDNSAsyncTransport()
|
||||
|
||||
|
||||
async def _get_with_safe_redirects(
|
||||
@@ -963,11 +963,16 @@ class WebFetchTool(Tool):
|
||||
is_valid, error_msg = _validate_url_safe(url)
|
||||
if not is_valid:
|
||||
return json.dumps({"error": f"URL validation failed: {error_msg}", "url": url}, ensure_ascii=False)
|
||||
if self.proxy:
|
||||
return json.dumps({
|
||||
"error": "web_fetch proxy is incompatible with DNS-pinned SSRF protection",
|
||||
"url": url,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
# Detect and fetch images directly to avoid Jina's textual image captioning
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
transport=_pinned_dns_transport(self.proxy),
|
||||
transport=_pinned_dns_transport(),
|
||||
timeout=15.0,
|
||||
) as client:
|
||||
r, stream, redirect_error = await _stream_with_safe_redirects(
|
||||
@@ -1040,7 +1045,7 @@ class WebFetchTool(Tool):
|
||||
try:
|
||||
async with httpx.AsyncClient(
|
||||
timeout=30.0,
|
||||
transport=_pinned_dns_transport(self.proxy),
|
||||
transport=_pinned_dns_transport(),
|
||||
) as client:
|
||||
r, redirect_error = await _get_with_safe_redirects(
|
||||
client,
|
||||
|
||||
@@ -161,11 +161,10 @@ class PinnedDNSAsyncTransport(httpx.AsyncBaseTransport):
|
||||
self,
|
||||
*,
|
||||
allow_loopback: bool = False,
|
||||
proxy: httpx.ProxyTypes | None = None,
|
||||
inner: httpx.AsyncBaseTransport | None = None,
|
||||
) -> None:
|
||||
self._allow_loopback = allow_loopback
|
||||
self._inner = inner or httpx.AsyncHTTPTransport(proxy=proxy)
|
||||
self._inner = inner or httpx.AsyncHTTPTransport()
|
||||
|
||||
async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
|
||||
url = str(request.url)
|
||||
|
||||
Reference in New Issue
Block a user