mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
Enforce Feishu webhook body limit while reading
This commit is contained in:
parent
e82d71db40
commit
a26680eb2d
2 changed files with 62 additions and 11 deletions
|
|
@ -228,6 +228,19 @@ _APPROVAL_LABEL_MAP: Dict[str, str] = {
|
|||
"always": "Approved permanently",
|
||||
"deny": "Denied",
|
||||
}
|
||||
|
||||
|
||||
async def _read_limited_feishu_webhook_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
|
||||
|
||||
|
||||
_FEISHU_BOT_MSG_TRACK_SIZE = 512 # LRU size for tracking sent message IDs
|
||||
_FEISHU_REPLY_FALLBACK_CODES = frozenset({230011, 231003}) # reply target withdrawn/missing → create fallback
|
||||
|
||||
|
|
@ -3467,9 +3480,16 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||
|
||||
try:
|
||||
body_bytes: bytes = await asyncio.wait_for(
|
||||
request.read(),
|
||||
_read_limited_feishu_webhook_body(
|
||||
request,
|
||||
_FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
||||
),
|
||||
timeout=_FEISHU_WEBHOOK_BODY_TIMEOUT_SECONDS,
|
||||
)
|
||||
except ValueError:
|
||||
logger.warning("[Feishu] Webhook body exceeds limit from %s", remote_ip)
|
||||
self._record_webhook_anomaly(remote_ip, "413")
|
||||
return web.Response(status=413, text="Request body too large")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("[Feishu] Webhook body read timed out after %ds from %s", _FEISHU_WEBHOOK_BODY_TIMEOUT_SECONDS, remote_ip)
|
||||
self._record_webhook_anomaly(remote_ip, "408")
|
||||
|
|
@ -3478,11 +3498,6 @@ class FeishuAdapter(BasePlatformAdapter):
|
|||
self._record_webhook_anomaly(remote_ip, "400")
|
||||
return web.json_response({"code": 400, "msg": "failed to read body"}, status=400)
|
||||
|
||||
if len(body_bytes) > _FEISHU_WEBHOOK_MAX_BODY_BYTES:
|
||||
logger.warning("[Feishu] Webhook body exceeds limit (%d bytes) from %s", len(body_bytes), remote_ip)
|
||||
self._record_webhook_anomaly(remote_ip, "413")
|
||||
return web.Response(status=413, text="Request body too large")
|
||||
|
||||
try:
|
||||
payload = json.loads(body_bytes.decode("utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
|
|
|
|||
|
|
@ -21,6 +21,18 @@ except ImportError:
|
|||
_HAS_LARK_OAPI = False
|
||||
|
||||
|
||||
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 _mock_event_dispatcher_builder(mock_handler_class):
|
||||
mock_builder = Mock()
|
||||
mock_builder.register_p2_im_message_message_read_v1 = Mock(return_value=mock_builder)
|
||||
|
|
@ -1604,7 +1616,7 @@ class TestAdapterBehavior(unittest.TestCase):
|
|||
remote="127.0.0.1",
|
||||
content_length=None,
|
||||
headers={},
|
||||
read=AsyncMock(return_value=body),
|
||||
content=_FakeRequestContent(body),
|
||||
)
|
||||
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
|
|
@ -1633,7 +1645,7 @@ class TestAdapterBehavior(unittest.TestCase):
|
|||
remote="203.0.113.10",
|
||||
content_length=None,
|
||||
headers={},
|
||||
read=AsyncMock(return_value=body),
|
||||
content=_FakeRequestContent(body),
|
||||
)
|
||||
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
|
|
@ -3261,6 +3273,30 @@ class TestWebhookSecurity(unittest.TestCase):
|
|||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
self.assertEqual(response.status, 413)
|
||||
|
||||
def test_webhook_request_rejects_oversized_chunked_body_while_reading(self):
|
||||
from gateway.config import PlatformConfig
|
||||
from hermes_constants import reset_hermes_home_override, set_hermes_home_override
|
||||
from plugins.platforms.feishu.adapter import FeishuAdapter, _FEISHU_WEBHOOK_MAX_BODY_BYTES
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
token = set_hermes_home_override(tmpdir)
|
||||
try:
|
||||
adapter = FeishuAdapter(PlatformConfig())
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
content = _FakeRequestContent(b"A" * (_FEISHU_WEBHOOK_MAX_BODY_BYTES + 2))
|
||||
request = SimpleNamespace(
|
||||
remote="127.0.0.1",
|
||||
content_length=None,
|
||||
headers={},
|
||||
content=content,
|
||||
)
|
||||
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
|
||||
self.assertEqual(response.status, 413)
|
||||
self.assertEqual(content.read_sizes, [_FEISHU_WEBHOOK_MAX_BODY_BYTES + 1])
|
||||
|
||||
@patch.dict(os.environ, {}, clear=True)
|
||||
def test_webhook_request_rejects_invalid_json(self):
|
||||
from gateway.config import PlatformConfig
|
||||
|
|
@ -3270,7 +3306,7 @@ class TestWebhookSecurity(unittest.TestCase):
|
|||
request = SimpleNamespace(
|
||||
remote="127.0.0.1",
|
||||
content_length=None,
|
||||
read=AsyncMock(return_value=b"not-json"),
|
||||
content=_FakeRequestContent(b"not-json"),
|
||||
)
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
self.assertEqual(response.status, 400)
|
||||
|
|
@ -3286,7 +3322,7 @@ class TestWebhookSecurity(unittest.TestCase):
|
|||
remote="127.0.0.1",
|
||||
content_length=None,
|
||||
headers={"x-lark-request-timestamp": "123", "x-lark-request-nonce": "abc", "x-lark-signature": "bad"},
|
||||
read=AsyncMock(return_value=body),
|
||||
content=_FakeRequestContent(body),
|
||||
)
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
self.assertEqual(response.status, 401)
|
||||
|
|
@ -3335,7 +3371,7 @@ class TestWebhookSecurity(unittest.TestCase):
|
|||
request = SimpleNamespace(
|
||||
remote="127.0.0.1",
|
||||
content_length=None,
|
||||
read=AsyncMock(return_value=body),
|
||||
content=_FakeRequestContent(body),
|
||||
)
|
||||
response = asyncio.run(adapter._handle_webhook_request(request))
|
||||
self.assertEqual(response.status, 200)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue