Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions plugins/platforms/line/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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):
Expand Down
42 changes: 42 additions & 0 deletions tests/plugins/test_line_body_limit.py
Original file line number Diff line number Diff line change
@@ -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