Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 3f7bc03

Browse files
committed
Apply extra checks to incoming requests
1 parent 97d2a63 commit 3f7bc03

File tree

4 files changed

+121
-2
lines changed

4 files changed

+121
-2
lines changed

synapse/api/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
"""Contains constants from the specification."""
1919

20+
# the max size of a (canonical-json-encoded) event
21+
MAX_PDU_SIZE = 65536
22+
2023
# the "depth" field on events is limited to 2**63 - 1
2124
MAX_DEPTH = 2 ** 63 - 1
2225

synapse/event_auth.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from signedjson.sign import SignatureVerifyException, verify_signed_json
2222
from unpaddedbase64 import decode_base64
2323

24-
from synapse.api.constants import EventTypes, JoinRules, Membership
24+
from synapse.api.constants import MAX_PDU_SIZE, EventTypes, JoinRules, Membership
2525
from synapse.api.errors import AuthError, EventSizeError, SynapseError
2626
from synapse.api.room_versions import (
2727
KNOWN_ROOM_VERSIONS,
@@ -205,7 +205,7 @@ def too_big(field):
205205
too_big("type")
206206
if len(event.event_id) > 255:
207207
too_big("event_id")
208-
if len(encode_canonical_json(event.get_pdu_json())) > 65536:
208+
if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE:
209209
too_big("event")
210210

211211

synapse/http/site.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from twisted.python.failure import Failure
2424
from twisted.web.server import Request, Site
2525

26+
from synapse.api.constants import MAX_PDU_SIZE
2627
from synapse.config.server import ListenerConfig
2728
from synapse.http import get_request_user_agent, redact_uri
2829
from synapse.http.request_metrics import RequestMetrics, requests_counter
@@ -59,6 +60,16 @@ class SynapseRequest(Request):
5960
logcontext: the log context for this request
6061
"""
6162

63+
# the biggest request we expect to see is a fully-loaded federation/send request.
64+
# Other than a bit of metadata, the main thing in such a request is up to 50 PDUs,
65+
# and up to 100 EDUs. PDUs are limited to 65535 bytes (possibly slightly more if
66+
# the sender didn't use canonical encoding); there is no specced limit to EDUs
67+
# (see https://github.com/matrix-org/matrix-doc/issues/3121).
68+
#
69+
# in short, we somewhat arbitrarily limit requests to 200 * 64K.
70+
#
71+
MAX_REQUEST_SIZE = 200 * MAX_PDU_SIZE
72+
6273
def __init__(self, channel, *args, **kw):
6374
Request.__init__(self, channel, *args, **kw)
6475
self.site = channel.site # type: SynapseSite
@@ -97,6 +108,16 @@ def __repr__(self):
97108
self.site.site_tag,
98109
)
99110

111+
def handleContentChunk(self, data):
112+
if self.content.tell() + len(data) > self.MAX_REQUEST_SIZE:
113+
logger.warning(
114+
"Aborting connection from %s because the request exceeds maximum size",
115+
self.client,
116+
)
117+
self.transport.abortConnection()
118+
return
119+
super().handleContentChunk(data)
120+
100121
@property
101122
def requester(self) -> Optional[Union[Requester, str]]:
102123
return self._requester

tests/http/test_site.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright 2021 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
import twisted.internet.base
15+
from twisted.internet.address import IPv6Address
16+
from twisted.internet.testing import StringTransport
17+
18+
from synapse.app.homeserver import SynapseHomeServer
19+
20+
from tests.unittest import HomeserverTestCase
21+
22+
23+
class SynapseRequestTestCase(HomeserverTestCase):
24+
def make_homeserver(self, reactor, clock):
25+
return self.setup_test_homeserver(homeserver_to_use=SynapseHomeServer)
26+
27+
def test_large_request(self):
28+
"""overlarge HTTP requests should be rejected"""
29+
self.hs.start_listening()
30+
31+
# find the HTTP server which is configured to listen on port 0
32+
(port, factory, _backlog, interface) = self.reactor.tcpServers[0]
33+
self.assertEqual(interface, "::")
34+
self.assertEqual(port, 0)
35+
36+
# as a control case, first send a regular request.
37+
38+
# complete the connection and wire it up to a fake transport
39+
client_address = IPv6Address("TCP", "::1", "2345")
40+
protocol = factory.buildProtocol(client_address)
41+
transport = StringTransport()
42+
protocol.makeConnection(transport)
43+
44+
protocol.dataReceived(
45+
b"POST / HTTP/1.1\r\n"
46+
b"Connection: close\r\n"
47+
b"Transfer-Encoding: chunked\r\n"
48+
b"\r\n"
49+
b"0\r\n"
50+
b"\r\n"
51+
)
52+
53+
while not transport.disconnecting:
54+
self.reactor.advance(1)
55+
56+
# we should get a 404
57+
self.assertRegex(transport.value().decode(), r"^HTTP/1\.1 404 ")
58+
59+
# now send an oversized request
60+
protocol = factory.buildProtocol(client_address)
61+
transport = StringTransport()
62+
protocol.makeConnection(transport)
63+
64+
protocol.dataReceived(
65+
b"POST / HTTP/1.1\r\n"
66+
b"Connection: close\r\n"
67+
b"Transfer-Encoding: chunked\r\n"
68+
b"\r\n"
69+
)
70+
71+
# we deliberately send all the data in one big chunk, to ensure that
72+
# twisted isn't buffering the data in the chunked transfer decoder.
73+
protocol.dataReceived(b"10000000\r\n")
74+
for i in range(0, 0x1000):
75+
protocol.dataReceived(b"\0" * 0x1000)
76+
77+
self.assertTrue(transport.disconnected, "connection did not drop")
78+
79+
# this is a bit of a hack. The problem is that the HTTPChannel sets a timeout
80+
# on receiving the request, which would normally be cleared once the entire
81+
# request is received. Of course, the entire request is *not* received, so
82+
# the timeout is never cancelled.
83+
#
84+
# Ideally then, we would tick the reactor past that timeout, to check what
85+
# happens. Unfortunately, twisted's `TimeoutMixin` (as used by HTTPChannel)
86+
# seems to be hardcoded to use the default twisted reactor, so the timeout is
87+
# set on the real reactor, which means we'd have to *actually* sleep here while
88+
# we wait for the timeout to elapse.
89+
#
90+
# So instead, let's just gut-wrench into twisted to cancel the timeout
91+
# ourselves.
92+
#
93+
# (Note that `protocol` here is actually a _GenericHTTPChannelProtocol - the
94+
# real HTTPChannel is `protocol._channel`)
95+
protocol._channel.setTimeout(None)

0 commit comments

Comments
 (0)