mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(security): guard remaining preflighted HTTP fetches
Several platform fetch paths called is_safe_url before constructing ordinary httpx clients, leaving a second DNS lookup at connection time. This preserved the rebinding window for Slack batch images, Feishu documents, Telegram URL-photo fallback, and WeCom remote media. Route each path through create_ssrf_safe_async_client and the shared redirect guard so direct connections validate and dial vetted IPs while configured proxies remain an explicit trusted egress boundary. Add per-path regressions that change DNS from public at preflight to metadata at connect time. The Skills Hub provenance fixture intentionally serves content over loopback. Opt that test-scoped server into private-address access so it keeps exercising the real HTTP transport without weakening production blocking. Related #8033 Co-authored-by: teknium1 <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
parent
42626da1ce
commit
0cd4afeafd
9 changed files with 268 additions and 20 deletions
|
|
@ -3446,13 +3446,17 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||
default_ext: str,
|
||||
preferred_name: str,
|
||||
) -> tuple[str, str]:
|
||||
from tools.url_safety import is_safe_url
|
||||
from gateway.platforms.base import _ssrf_redirect_guard
|
||||
from tools.url_safety import create_ssrf_safe_async_client, is_safe_url
|
||||
|
||||
if not is_safe_url(file_url):
|
||||
raise ValueError(f"Blocked unsafe URL (SSRF protection): {file_url[:80]}")
|
||||
|
||||
import httpx
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
||||
async with create_ssrf_safe_async_client(
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
event_hooks={"response": [_ssrf_redirect_guard]},
|
||||
) as client:
|
||||
response = await client.get(
|
||||
file_url,
|
||||
headers={
|
||||
|
|
|
|||
|
|
@ -2906,9 +2906,12 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
chat_id, team_id=self._metadata_team_id(metadata)
|
||||
)
|
||||
try:
|
||||
import httpx as _httpx
|
||||
from urllib.parse import unquote as _unquote
|
||||
from tools.url_safety import is_safe_url as _is_safe_url
|
||||
from gateway.platforms.base import _ssrf_redirect_guard
|
||||
from tools.url_safety import (
|
||||
create_ssrf_safe_async_client,
|
||||
is_safe_url as _is_safe_url,
|
||||
)
|
||||
except Exception:
|
||||
await super().send_multiple_images(chat_id, images, metadata, human_delay)
|
||||
return
|
||||
|
|
@ -2925,7 +2928,7 @@ class SlackAdapter(BasePlatformAdapter):
|
|||
file_uploads: List[Dict[str, Any]] = []
|
||||
initial_comment_parts: List[str] = []
|
||||
try:
|
||||
async with _httpx.AsyncClient(
|
||||
async with create_ssrf_safe_async_client(
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
event_hooks={"response": [_ssrf_redirect_guard]},
|
||||
|
|
|
|||
|
|
@ -6925,8 +6925,13 @@ class TelegramAdapter(BasePlatformAdapter):
|
|||
)
|
||||
# Fallback: download and upload as file (supports up to 10MB)
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
from gateway.platforms.base import _ssrf_redirect_guard
|
||||
from tools.url_safety import create_ssrf_safe_async_client
|
||||
|
||||
async with create_ssrf_safe_async_client(
|
||||
timeout=30.0,
|
||||
event_hooks={"response": [_ssrf_redirect_guard]},
|
||||
) as client:
|
||||
resp = await client.get(image_url)
|
||||
resp.raise_for_status()
|
||||
image_data = resp.content
|
||||
|
|
|
|||
|
|
@ -219,8 +219,14 @@ class WeComAdapter(BasePlatformAdapter):
|
|||
try:
|
||||
# Tighter keepalive so idle CLOSE_WAIT drains promptly (#18451).
|
||||
from gateway.platforms._http_client_limits import platform_httpx_limits
|
||||
self._http_client = httpx.AsyncClient(
|
||||
timeout=30.0, follow_redirects=True, limits=platform_httpx_limits(),
|
||||
from gateway.platforms.base import _ssrf_redirect_guard
|
||||
from tools.url_safety import create_ssrf_safe_async_client
|
||||
|
||||
self._http_client = create_ssrf_safe_async_client(
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
event_hooks={"response": [_ssrf_redirect_guard]},
|
||||
limits=platform_httpx_limits(),
|
||||
)
|
||||
await self._open_connection()
|
||||
self._mark_connected()
|
||||
|
|
@ -1095,14 +1101,20 @@ class WeComAdapter(BasePlatformAdapter):
|
|||
url: str,
|
||||
max_bytes: int,
|
||||
) -> Tuple[bytes, Dict[str, str]]:
|
||||
from tools.url_safety import is_safe_url
|
||||
from gateway.platforms.base import _ssrf_redirect_guard
|
||||
from tools.url_safety import create_ssrf_safe_async_client, is_safe_url
|
||||
|
||||
if not is_safe_url(url):
|
||||
raise ValueError(f"Blocked unsafe URL (SSRF protection): {url[:80]}")
|
||||
|
||||
if not HTTPX_AVAILABLE:
|
||||
raise RuntimeError("httpx is required for WeCom media download")
|
||||
|
||||
client = self._http_client or httpx.AsyncClient(timeout=30.0, follow_redirects=True)
|
||||
client = self._http_client or create_ssrf_safe_async_client(
|
||||
timeout=30.0,
|
||||
follow_redirects=True,
|
||||
event_hooks={"response": [_ssrf_redirect_guard]},
|
||||
)
|
||||
created_client = client is not self._http_client
|
||||
try:
|
||||
async with client.stream(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue