diff --git a/plugins/platforms/line/adapter.py b/plugins/platforms/line/adapter.py index 447cf5fb0c800..751f1243363d5 100644 --- a/plugins/platforms/line/adapter.py +++ b/plugins/platforms/line/adapter.py @@ -276,6 +276,17 @@ def verify_line_signature(body: bytes, signature: str, channel_secret: str) -> b return hmac.compare_digest(expected, signature) +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 + + # --------------------------------------------------------------------------- # Cache state machine — slow-LLM postback flow # --------------------------------------------------------------------------- @@ -869,12 +880,12 @@ async def _handle_webhook(self, request) -> Any: # Body cap defends against memory-exhaustion via crafted Content-Length # (aiohttp's client_max_size only applies to certain body modes). try: - body = await request.read() + body = await _read_limited_request_body(request, WEBHOOK_BODY_MAX_BYTES) + except ValueError: + return web.Response(status=413, text="payload too large") except Exception as exc: logger.debug("LINE: read failed: %s", exc) return web.Response(status=400, text="bad request") - if len(body) > WEBHOOK_BODY_MAX_BYTES: - return web.Response(status=413, text="payload too large") signature = request.headers.get("X-Line-Signature", "") if not verify_line_signature(body, signature, self.channel_secret): diff --git a/tests/plugins/test_line_body_limit.py b/tests/plugins/test_line_body_limit.py new file mode 100644 index 0000000000000..73caeb3f401a5 --- /dev/null +++ b/tests/plugins/test_line_body_limit.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from plugins.platforms.line.adapter import _read_limited_request_body + + +class _Content: + def __init__(self, body: bytes): + self.body = body + self.read_size = None + + async def readexactly(self, size: int) -> bytes: + self.read_size = size + if len(self.body) < size: + raise asyncio.IncompleteReadError(self.body, size) + return self.body[:size] + + +class _Request: + def __init__(self, body: bytes): + self.content = _Content(body) + + +def test_read_limited_request_body_returns_short_body(): + request = _Request(b'{"events":[]}') + + body = asyncio.run(_read_limited_request_body(request, 32)) + + assert body == b'{"events":[]}' + assert request.content.read_size == 33 + + +def test_read_limited_request_body_rejects_oversized_body(): + request = _Request(b"x" * 33) + + with pytest.raises(ValueError, match="payload too large"): + asyncio.run(_read_limited_request_body(request, 32)) + + assert request.content.read_size == 33