mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
Enforce WhatsApp Cloud webhook body limit while reading
This commit is contained in:
parent
deae37e33b
commit
eec92a92c0
2 changed files with 39 additions and 10 deletions
|
|
@ -90,6 +90,7 @@ DEFAULT_WEBHOOK_HOST = "0.0.0.0"
|
|||
DEFAULT_WEBHOOK_PORT = 8090
|
||||
DEFAULT_WEBHOOK_PATH = "/whatsapp/webhook"
|
||||
GRAPH_API_BASE = "https://graph.facebook.com"
|
||||
WEBHOOK_MAX_BODY_BYTES = 3 * 1024 * 1024
|
||||
# Meta retries failed webhooks for up to 7 days. We don't need to remember
|
||||
# every wamid for the full retry window — the practical risk is duplicate
|
||||
# delivery within minutes, not days. 5000 entries with FIFO eviction is
|
||||
|
|
@ -144,6 +145,17 @@ _WHATSAPP_MIME_EXTENSION_OVERRIDES: Dict[str, str] = {
|
|||
}
|
||||
|
||||
|
||||
async def _read_limited_request_body(request: Any, max_bytes: int) -> bytes:
|
||||
"""Read at most ``max_bytes`` from an aiohttp request body."""
|
||||
try:
|
||||
body = await request.content.readexactly(max_bytes + 1)
|
||||
except asyncio.IncompleteReadError as exc:
|
||||
body = exc.partial
|
||||
if len(body) > max_bytes:
|
||||
raise ValueError("payload too large")
|
||||
return body
|
||||
|
||||
|
||||
def _ext_for_mime(mime: str) -> Optional[str]:
|
||||
"""Resolve a mime type to the file extension we want on disk.
|
||||
|
||||
|
|
@ -1405,16 +1417,18 @@ class WhatsAppCloudAdapter(WhatsAppBehaviorMixin, BasePlatformAdapter):
|
|||
multiply downstream agent work because of a transient bug
|
||||
during dispatch.
|
||||
"""
|
||||
# Meta's documented max payload is 3MB. Read one byte past the limit
|
||||
# so oversized chunked bodies are rejected before buffering the rest.
|
||||
try:
|
||||
raw = await request.read()
|
||||
raw = await _read_limited_request_body(
|
||||
request,
|
||||
WEBHOOK_MAX_BODY_BYTES,
|
||||
)
|
||||
except ValueError:
|
||||
return web.Response(status=413)
|
||||
except Exception:
|
||||
return web.Response(status=400)
|
||||
|
||||
# Meta's documented max payload is 3MB. Reject earlier than aiohttp
|
||||
# would so we don't even compute HMAC over giant junk.
|
||||
if len(raw) > 3 * 1024 * 1024:
|
||||
return web.Response(status=413)
|
||||
|
||||
# Refuse to accept anything if app_secret isn't configured. Without
|
||||
# it we can't authenticate the sender, and the handler would be a
|
||||
# data-injection point. Same defensive posture as the GET verify
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ exercised with synthetic ``Request`` objects.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
|
@ -405,10 +406,22 @@ def _sign(secret: str, body: bytes) -> str:
|
|||
return f"sha256={digest}"
|
||||
|
||||
|
||||
class _FakeRequestContent:
|
||||
def __init__(self, body: bytes):
|
||||
self.body = body
|
||||
self.read_sizes: list[int] = []
|
||||
|
||||
async def readexactly(self, size: int) -> bytes:
|
||||
self.read_sizes.append(size)
|
||||
if len(self.body) < size:
|
||||
raise asyncio.IncompleteReadError(self.body, size)
|
||||
return self.body[:size]
|
||||
|
||||
|
||||
def _post_request(body: bytes, headers: dict | None = None):
|
||||
"""Build a minimal aiohttp.web.Request stub for POST tests."""
|
||||
request = MagicMock()
|
||||
request.read = AsyncMock(return_value=body)
|
||||
request.content = _FakeRequestContent(body)
|
||||
request.headers = headers or {}
|
||||
return request
|
||||
|
||||
|
|
@ -539,20 +552,23 @@ class TestWebhookSignature:
|
|||
@pytest.mark.asyncio
|
||||
async def test_oversize_body_rejected_before_signature(self):
|
||||
"""3MB cap per Meta — refuse without computing HMAC over giant junk."""
|
||||
from gateway.platforms.whatsapp_cloud import WEBHOOK_MAX_BODY_BYTES
|
||||
|
||||
adapter = _make_adapter(app_secret="key")
|
||||
adapter._dispatch_payload = AsyncMock()
|
||||
body = b"x" * (4 * 1024 * 1024)
|
||||
body = b"x" * (WEBHOOK_MAX_BODY_BYTES + 2)
|
||||
request = _post_request(body, {"X-Hub-Signature-256": "sha256=ignored"})
|
||||
|
||||
response = await adapter._handle_webhook(request)
|
||||
assert response.status == 413
|
||||
assert request.content.read_sizes == [WEBHOOK_MAX_BODY_BYTES + 1]
|
||||
adapter._dispatch_payload.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unreadable_body_rejected(self):
|
||||
adapter = _make_adapter(app_secret="key")
|
||||
request = MagicMock()
|
||||
request.read = AsyncMock(side_effect=RuntimeError("read failed"))
|
||||
request.content.readexactly = AsyncMock(side_effect=RuntimeError("read failed"))
|
||||
request.headers = {}
|
||||
|
||||
response = await adapter._handle_webhook(request)
|
||||
|
|
@ -2436,4 +2452,3 @@ class TestReplyContextResolution:
|
|||
rich_sent_store.lookup("15551234567", "wamid.OUT")
|
||||
== "here is your answer"
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue