diff --git a/plugins/platforms/sms/adapter.py b/plugins/platforms/sms/adapter.py index e40ec0ac39a..1646b1b5956 100644 --- a/plugins/platforms/sms/adapter.py +++ b/plugins/platforms/sms/adapter.py @@ -42,6 +42,7 @@ TWILIO_API_BASE = "https://api.twilio.com/2010-04-01/Accounts" MAX_SMS_LENGTH = 1600 # ~10 SMS segments DEFAULT_WEBHOOK_PORT = 8080 DEFAULT_WEBHOOK_HOST = "127.0.0.1" +_TWILIO_WEBHOOK_MAX_BODY_BYTES = 65_536 # 64 KiB — Twilio payloads are small def check_sms_requirements() -> bool: @@ -293,7 +294,20 @@ class SmsAdapter(BasePlatformAdapter): from aiohttp import web try: + content_length = request.content_length + if content_length is not None and content_length > _TWILIO_WEBHOOK_MAX_BODY_BYTES: + return web.Response( + text='', + content_type="application/xml", + status=413, + ) raw = await request.read() + if len(raw) > _TWILIO_WEBHOOK_MAX_BODY_BYTES: + return web.Response( + text='', + content_type="application/xml", + status=413, + ) # Twilio sends form-encoded data, not JSON form = urllib.parse.parse_qs(raw.decode("utf-8"), keep_blank_values=True) except Exception as e: diff --git a/tests/gateway/test_sms.py b/tests/gateway/test_sms.py index 85a9501f06a..bfb70289b75 100644 --- a/tests/gateway/test_sms.py +++ b/tests/gateway/test_sms.py @@ -459,10 +459,11 @@ class TestWebhookSignatureEnforcement: adapter._message_handler = AsyncMock() return adapter - def _mock_request(self, body, headers=None): + def _mock_request(self, body, headers=None, content_length=None): request = MagicMock() request.read = AsyncMock(return_value=body) request.headers = headers or {} + request.content_length = content_length return request @pytest.mark.asyncio @@ -536,3 +537,27 @@ class TestWebhookSignatureEnforcement: request = self._mock_request(body, headers={"X-Twilio-Signature": sig}) resp = await adapter._handle_webhook(request) assert resp.status == 200 + + @pytest.mark.asyncio + async def test_webhook_rejects_oversized_body_via_content_length(self): + """POST with Content-Length exceeding 64 KiB returns 413 before reading.""" + adapter = self._make_adapter(webhook_url="") + body = b"From=%2B15551234567&To=%2B15550001111&Body=hello&MessageSid=SM123" + request = self._mock_request(body, content_length=65_537) + resp = await adapter._handle_webhook(request) + assert resp.status == 413 + # request.read must NOT have been called — we bailed on Content-Length + request.read.assert_not_called() + + @pytest.mark.asyncio + async def test_webhook_rejects_oversized_body_via_read_length(self): + """POST whose actual read size exceeds 64 KiB returns 413. + + Covers the case where Content-Length is absent (chunked transfer) but + the body still exceeds the cap. + """ + adapter = self._make_adapter(webhook_url="") + oversized = b"x" * 65_537 + request = self._mock_request(oversized, content_length=None) + resp = await adapter._handle_webhook(request) + assert resp.status == 413