mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
fix(msgraph): enforce webhook body limits
This commit is contained in:
parent
3dd5ce236a
commit
4f4cbff8bd
2 changed files with 94 additions and 3 deletions
|
|
@ -34,6 +34,7 @@ DEFAULT_HOST = "0.0.0.0"
|
|||
DEFAULT_PORT = 8646
|
||||
DEFAULT_WEBHOOK_PATH = "/msgraph/webhook"
|
||||
DEFAULT_MAX_SEEN_RECEIPTS = 5000
|
||||
DEFAULT_MAX_BODY_BYTES = 1_048_576
|
||||
NotificationScheduler = Callable[[Dict[str, Any], MessageEvent], Awaitable[None] | None]
|
||||
|
||||
|
||||
|
|
@ -63,6 +64,9 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
|
|||
self._max_seen_receipts = max(
|
||||
1, int(extra.get("max_seen_receipts", DEFAULT_MAX_SEEN_RECEIPTS))
|
||||
)
|
||||
self._max_body_bytes = max(
|
||||
1, int(extra.get("max_body_bytes", DEFAULT_MAX_BODY_BYTES))
|
||||
)
|
||||
self._allowed_source_networks: list[ipaddress._BaseNetwork] = (
|
||||
self._parse_allowed_source_cidrs(extra.get("allowed_source_cidrs"))
|
||||
)
|
||||
|
|
@ -152,7 +156,7 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
|
|||
)
|
||||
return False
|
||||
|
||||
app = web.Application()
|
||||
app = web.Application(client_max_size=self._max_body_bytes)
|
||||
app.router.add_get(self._health_path, self._handle_health)
|
||||
app.router.add_get(self._webhook_path, self._handle_validation)
|
||||
app.router.add_post(self._webhook_path, self._handle_notification)
|
||||
|
|
@ -229,8 +233,24 @@ class MSGraphWebhookAdapter(BasePlatformAdapter):
|
|||
return web.Response(text=validation_token, content_type="text/plain")
|
||||
|
||||
try:
|
||||
body = await request.json()
|
||||
content_length = request.content_length
|
||||
except Exception:
|
||||
content_length = None
|
||||
if content_length is not None and content_length > self._max_body_bytes:
|
||||
return web.Response(status=413)
|
||||
|
||||
try:
|
||||
raw_body = await request.read()
|
||||
except Exception:
|
||||
return web.Response(status=400)
|
||||
if len(raw_body) > self._max_body_bytes:
|
||||
return web.Response(status=413)
|
||||
|
||||
try:
|
||||
body = json.loads(raw_body.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return web.Response(status=400)
|
||||
if not isinstance(body, dict):
|
||||
return web.Response(status=400)
|
||||
|
||||
notifications = body.get("value")
|
||||
|
|
|
|||
|
|
@ -19,9 +19,19 @@ def _make_adapter(**extra_overrides) -> MSGraphWebhookAdapter:
|
|||
|
||||
|
||||
class _FakeRequest:
|
||||
def __init__(self, *, query=None, json_payload=None, remote="127.0.0.1"):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
query=None,
|
||||
json_payload=None,
|
||||
raw_body: bytes | None = None,
|
||||
content_length: int | None = None,
|
||||
remote="127.0.0.1",
|
||||
):
|
||||
self.query = query or {}
|
||||
self._json_payload = json_payload
|
||||
self._raw_body = raw_body
|
||||
self.content_length = content_length
|
||||
self.remote = remote
|
||||
|
||||
async def json(self):
|
||||
|
|
@ -29,6 +39,11 @@ class _FakeRequest:
|
|||
raise self._json_payload
|
||||
return self._json_payload
|
||||
|
||||
async def read(self):
|
||||
if self._raw_body is not None:
|
||||
return self._raw_body
|
||||
return json.dumps(self._json_payload or {}).encode("utf-8")
|
||||
|
||||
|
||||
class TestMSGraphWebhookConfig:
|
||||
def test_gateway_config_accepts_msgraph_webhook_platform(self):
|
||||
|
|
@ -183,6 +198,62 @@ class TestMSGraphNotifications:
|
|||
assert event.source.chat_type == "webhook"
|
||||
assert event.message_id == "id:notif-1"
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_oversized_notification_rejected_by_content_length(self):
|
||||
adapter = _make_adapter(max_body_bytes=100)
|
||||
payload = {
|
||||
"value": [
|
||||
{
|
||||
"id": "notif-oversized",
|
||||
"subscriptionId": "sub-1",
|
||||
"changeType": "updated",
|
||||
"resource": "communications/onlineMeetings/meeting-1",
|
||||
"clientState": "expected-client-state",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
resp = await adapter._handle_notification(
|
||||
_FakeRequest(json_payload=payload, content_length=101)
|
||||
)
|
||||
|
||||
assert resp.status == 413
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_chunked_oversized_notification_rejected_after_read(self):
|
||||
adapter = _make_adapter(max_body_bytes=100)
|
||||
payload = {
|
||||
"value": [
|
||||
{
|
||||
"id": "notif-chunked-oversized",
|
||||
"subscriptionId": "sub-1",
|
||||
"changeType": "updated",
|
||||
"resource": "communications/onlineMeetings/meeting-1",
|
||||
"clientState": "expected-client-state",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
resp = await adapter._handle_notification(
|
||||
_FakeRequest(
|
||||
json_payload=payload,
|
||||
raw_body=b"x" * 101,
|
||||
content_length=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert resp.status == 413
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_non_object_notification_body_rejected(self):
|
||||
adapter = _make_adapter()
|
||||
|
||||
resp = await adapter._handle_notification(
|
||||
_FakeRequest(json_payload=[], raw_body=b"[]")
|
||||
)
|
||||
|
||||
assert resp.status == 400
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_bad_client_state_rejected_as_auth_failure(self):
|
||||
"""Every-item-bad-clientState batches return 403 so forged POSTs stop retrying."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue