mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
security(gateway): set explicit client_max_size on 3 uncapped aiohttp servers (#59180)
Sibling sweep from the #58902 raft review found aiohttp servers still running on the implicit 1 MiB default with no explicit body cap: - bluebubbles webhook (127.0.0.1): 1 MiB explicit cap — events are small JSON/form payloads; attachments arrive via the REST API - teams Bot Framework listener (0.0.0.0 bind — most exposed): 1 MiB cap; activities are JSON well under that - hermes proxy server: 10 MB cap mirroring api_server's MAX_REQUEST_BYTES (chat-completion payloads can be large, but must stay bounded) client_max_size bounds every read path including chunked transfer-encoding requests that carry no Content-Length (#58536/#58902 pattern). Deliberately excluded: feishu, whatsapp_cloud, sms, line, wecom, msgraph — open contributor PRs (#54938, #54944, #54620, #54931, #54934, #25296) already cover those; reviewing them separately preserves their credit. 3 regression tests pin the wiring.
This commit is contained in:
parent
0823230545
commit
8986981df4
4 changed files with 59 additions and 4 deletions
|
|
@ -40,6 +40,10 @@ logger = logging.getLogger(__name__)
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_WEBHOOK_HOST = "127.0.0.1"
|
||||
# BlueBubbles webhook events are small JSON/form payloads; attachments come
|
||||
# through the REST API, not the webhook. 1 MiB is generous headroom while
|
||||
# keeping oversized/chunked bodies from being buffered unbounded.
|
||||
_WEBHOOK_MAX_BODY_BYTES = 1_048_576
|
||||
DEFAULT_WEBHOOK_PORT = 8645
|
||||
DEFAULT_WEBHOOK_PATH = "/bluebubbles-webhook"
|
||||
MAX_TEXT_LENGTH = 4000
|
||||
|
|
@ -264,7 +268,11 @@ class BlueBubblesAdapter(BasePlatformAdapter):
|
|||
self.client = None
|
||||
return False
|
||||
|
||||
app = web.Application()
|
||||
# Explicit body cap: BlueBubbles webhook events are small JSON (or
|
||||
# form-encoded) payloads. client_max_size makes aiohttp enforce the
|
||||
# cap on every read path — including chunked requests that carry no
|
||||
# Content-Length (same pattern as webhook.py / raft, #58536/#58902).
|
||||
app = web.Application(client_max_size=_WEBHOOK_MAX_BODY_BYTES)
|
||||
app.router.add_get("/health", lambda _: web.Response(text="ok"))
|
||||
app.router.add_post(self.webhook_path, self._handle_webhook)
|
||||
# The webhook auth value is carried in the query string because the
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ _HOP_BY_HOP_HEADERS = frozenset(
|
|||
|
||||
DEFAULT_PORT = 8645
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
# Body cap for forwarded requests. Chat-completion payloads with long agent
|
||||
# conversations can be large; mirror api_server's MAX_REQUEST_BYTES (10 MB).
|
||||
# client_max_size bounds every read path, including chunked bodies.
|
||||
MAX_REQUEST_BYTES = 10_000_000
|
||||
|
||||
|
||||
def _json_error(status: int, message: str, code: str = "proxy_error") -> "web.Response":
|
||||
|
|
@ -89,7 +93,7 @@ def create_app(adapter: UpstreamAdapter) -> "web.Application":
|
|||
"pip install 'hermes-agent[messaging]' or `pip install aiohttp`."
|
||||
)
|
||||
|
||||
app = web.Application()
|
||||
app = web.Application(client_max_size=MAX_REQUEST_BYTES)
|
||||
# AppKey ensures forward-compat with future aiohttp versions that strip
|
||||
# bare-string keys.
|
||||
_adapter_key = web.AppKey("adapter", UpstreamAdapter)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ from gateway.platforms.base import (
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_PORT = 3978
|
||||
# Bot Framework activities are JSON payloads well under 1 MiB; an explicit
|
||||
# aiohttp client_max_size keeps oversized/chunked request bodies bounded.
|
||||
_MAX_BODY_BYTES = 1_048_576
|
||||
_WEBHOOK_PATH = "/api/messages"
|
||||
|
||||
|
||||
|
|
@ -738,8 +741,12 @@ class TeamsAdapter(BasePlatformAdapter):
|
|||
return False
|
||||
|
||||
try:
|
||||
# Set up aiohttp app first — the bridge adapter wires SDK routes into it
|
||||
aiohttp_app = web.Application()
|
||||
# Set up aiohttp app first — the bridge adapter wires SDK routes into it.
|
||||
# client_max_size: Bot Framework activities are JSON (caps out well
|
||||
# under 1 MiB); an explicit cap keeps oversized/chunked bodies from
|
||||
# being buffered unbounded on a 0.0.0.0 bind (same pattern as
|
||||
# webhook.py / raft, #58536/#58902).
|
||||
aiohttp_app = web.Application(client_max_size=_MAX_BODY_BYTES)
|
||||
aiohttp_app.router.add_get("/health", lambda _: web.Response(text="ok"))
|
||||
|
||||
self._app = App(
|
||||
|
|
|
|||
36
tests/gateway/test_aiohttp_body_caps.py
Normal file
36
tests/gateway/test_aiohttp_body_caps.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
"""Regression tests: aiohttp servers must set an explicit ``client_max_size``.
|
||||
|
||||
Without it, aiohttp falls back to its implicit 1 MiB default and — worse —
|
||||
handlers that only check ``Content-Length`` can be bypassed entirely by
|
||||
chunked transfer-encoding requests (#58536 webhook, #58902 raft lineage).
|
||||
These tests pin the wiring for the three servers fixed in this follow-up:
|
||||
bluebubbles, teams, and the ``hermes proxy`` server.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
|
||||
def test_bluebubbles_app_sets_client_max_size():
|
||||
import gateway.platforms.bluebubbles as bb
|
||||
|
||||
assert bb._WEBHOOK_MAX_BODY_BYTES > 0
|
||||
src = inspect.getsource(bb.BlueBubblesAdapter.connect)
|
||||
assert "client_max_size=_WEBHOOK_MAX_BODY_BYTES" in src
|
||||
|
||||
|
||||
def test_teams_app_sets_client_max_size():
|
||||
import plugins.platforms.teams.adapter as teams
|
||||
|
||||
assert teams._MAX_BODY_BYTES > 0
|
||||
src = inspect.getsource(teams.TeamsAdapter.connect)
|
||||
assert "client_max_size=_MAX_BODY_BYTES" in src
|
||||
|
||||
|
||||
def test_proxy_app_sets_client_max_size():
|
||||
import hermes_cli.proxy.server as proxy_server
|
||||
|
||||
# Mirrors api_server's MAX_REQUEST_BYTES: chat payloads can be large,
|
||||
# but the cap must exist so chunked bodies stay bounded.
|
||||
assert proxy_server.MAX_REQUEST_BYTES >= 1_048_576
|
||||
src = inspect.getsource(proxy_server.create_app)
|
||||
assert "client_max_size=MAX_REQUEST_BYTES" in src
|
||||
Loading…
Add table
Add a link
Reference in a new issue