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
14 changes: 14 additions & 0 deletions plugins/platforms/sms/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
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:
Expand Down Expand Up @@ -293,7 +294,20 @@ async def _handle_webhook(self, request) -> "aiohttp.web.Response":
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='<?xml version="1.0" encoding="UTF-8"?><Response></Response>',
content_type="application/xml",
status=413,
)
raw = await request.read()
if len(raw) > _TWILIO_WEBHOOK_MAX_BODY_BYTES:
return web.Response(
text='<?xml version="1.0" encoding="UTF-8"?><Response></Response>',
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:
Expand Down
27 changes: 26 additions & 1 deletion tests/gateway/test_sms.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,10 +459,11 @@ def _make_adapter(self, webhook_url=""):
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
Expand Down Expand Up @@ -536,3 +537,27 @@ async def test_port_variant_signature_returns_200(self):
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