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

Commit 3ff2251

Browse files
authored
Improved validation for received requests (#9817)
* Simplify `start_listening` callpath * Correctly check the size of uploaded files
1 parent 84936e2 commit 3ff2251

File tree

15 files changed

+174
-31
lines changed

15 files changed

+174
-31
lines changed

changelog.d/9817.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug which caused `max_upload_size` to not be correctly enforced.

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/app/_base.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@
3030
from twisted.protocols.tls import TLSMemoryBIOFactory
3131

3232
import synapse
33+
from synapse.api.constants import MAX_PDU_SIZE
3334
from synapse.app import check_bind_error
3435
from synapse.app.phone_stats_home import start_phone_stats_home
35-
from synapse.config.server import ListenerConfig
36+
from synapse.config.homeserver import HomeServerConfig
3637
from synapse.crypto import context_factory
3738
from synapse.logging.context import PreserveLoggingContext
3839
from synapse.metrics.background_process_metrics import wrap_as_background_process
@@ -288,7 +289,7 @@ def refresh_certificate(hs):
288289
logger.info("Context factories updated.")
289290

290291

291-
async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]):
292+
async def start(hs: "synapse.server.HomeServer"):
292293
"""
293294
Start a Synapse server or worker.
294295
@@ -300,7 +301,6 @@ async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerCon
300301
301302
Args:
302303
hs: homeserver instance
303-
listeners: Listener configuration ('listeners' in homeserver.yaml)
304304
"""
305305
# Set up the SIGHUP machinery.
306306
if hasattr(signal, "SIGHUP"):
@@ -336,7 +336,7 @@ def run_sighup(*args, **kwargs):
336336
synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa
337337

338338
# It is now safe to start your Synapse.
339-
hs.start_listening(listeners)
339+
hs.start_listening()
340340
hs.get_datastore().db_pool.start_profiling()
341341
hs.get_pusherpool().start()
342342

@@ -530,3 +530,25 @@ def sdnotify(state):
530530
# this is a bit surprising, since we don't expect to have a NOTIFY_SOCKET
531531
# unless systemd is expecting us to notify it.
532532
logger.warning("Unable to send notification to systemd: %s", e)
533+
534+
535+
def max_request_body_size(config: HomeServerConfig) -> int:
536+
"""Get a suitable maximum size for incoming HTTP requests"""
537+
538+
# Other than media uploads, the biggest request we expect to see is a fully-loaded
539+
# /federation/v1/send request.
540+
#
541+
# The main thing in such a request is up to 50 PDUs, and up to 100 EDUs. PDUs are
542+
# limited to 65536 bytes (possibly slightly more if the sender didn't use canonical
543+
# json encoding); there is no specced limit to EDUs (see
544+
# https://github.com/matrix-org/matrix-doc/issues/3121).
545+
#
546+
# in short, we somewhat arbitrarily limit requests to 200 * 64K (about 12.5M)
547+
#
548+
max_request_size = 200 * MAX_PDU_SIZE
549+
550+
# if we have a media repo enabled, we may need to allow larger uploads than that
551+
if config.media.can_load_media_repo:
552+
max_request_size = max(max_request_size, config.media.max_upload_size)
553+
554+
return max_request_size

synapse/app/admin_cmd.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,6 @@ class AdminCmdSlavedStore(
7070
class AdminCmdServer(HomeServer):
7171
DATASTORE_CLASS = AdminCmdSlavedStore
7272

73-
def _listen_http(self, listener_config):
74-
pass
75-
76-
def start_listening(self, listeners):
77-
pass
78-
7973

8074
async def export_data_command(hs, args):
8175
"""Export data for a user.
@@ -232,7 +226,7 @@ def start(config_options):
232226

233227
async def run():
234228
with LoggingContext("command"):
235-
_base.start(ss, [])
229+
_base.start(ss)
236230
await args.func(ss, args)
237231

238232
_base.start_worker_reactor(

synapse/app/generic_worker.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# limitations under the License.
1616
import logging
1717
import sys
18-
from typing import Dict, Iterable, Optional
18+
from typing import Dict, Optional
1919

2020
from twisted.internet import address
2121
from twisted.web.resource import IResource
@@ -32,7 +32,7 @@
3232
SERVER_KEY_V2_PREFIX,
3333
)
3434
from synapse.app import _base
35-
from synapse.app._base import register_start
35+
from synapse.app._base import max_request_body_size, register_start
3636
from synapse.config._base import ConfigError
3737
from synapse.config.homeserver import HomeServerConfig
3838
from synapse.config.logger import setup_logging
@@ -367,15 +367,16 @@ def _listen_http(self, listener_config: ListenerConfig):
367367
listener_config,
368368
root_resource,
369369
self.version_string,
370+
max_request_body_size=max_request_body_size(self.config),
370371
reactor=self.get_reactor(),
371372
),
372373
reactor=self.get_reactor(),
373374
)
374375

375376
logger.info("Synapse worker now listening on port %d", port)
376377

377-
def start_listening(self, listeners: Iterable[ListenerConfig]):
378-
for listener in listeners:
378+
def start_listening(self):
379+
for listener in self.config.worker_listeners:
379380
if listener.type == "http":
380381
self._listen_http(listener)
381382
elif listener.type == "manhole":
@@ -468,7 +469,7 @@ def start(config_options):
468469
# streams. Will no-op if no streams can be written to by this worker.
469470
hs.get_replication_streamer()
470471

471-
register_start(_base.start, hs, config.worker_listeners)
472+
register_start(_base.start, hs)
472473

473474
_base.start_worker_reactor("synapse-generic-worker", config)
474475

synapse/app/homeserver.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import logging
1818
import os
1919
import sys
20-
from typing import Iterable, Iterator
20+
from typing import Iterator
2121

2222
from twisted.internet import reactor
2323
from twisted.web.resource import EncodingResourceWrapper, IResource
@@ -36,7 +36,13 @@
3636
WEB_CLIENT_PREFIX,
3737
)
3838
from synapse.app import _base
39-
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error, register_start
39+
from synapse.app._base import (
40+
listen_ssl,
41+
listen_tcp,
42+
max_request_body_size,
43+
quit_with_error,
44+
register_start,
45+
)
4046
from synapse.config._base import ConfigError
4147
from synapse.config.emailconfig import ThreepidBehaviour
4248
from synapse.config.homeserver import HomeServerConfig
@@ -132,6 +138,7 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf
132138
listener_config,
133139
create_resource_tree(resources, root_resource),
134140
self.version_string,
141+
max_request_body_size=max_request_body_size(self.config),
135142
reactor=self.get_reactor(),
136143
)
137144

@@ -268,14 +275,14 @@ def _configure_named_resource(self, name, compress=False):
268275

269276
return resources
270277

271-
def start_listening(self, listeners: Iterable[ListenerConfig]):
278+
def start_listening(self):
272279
if self.config.redis_enabled:
273280
# If redis is enabled we connect via the replication command handler
274281
# in the same way as the workers (since we're effectively a client
275282
# rather than a server).
276283
self.get_tcp_replication().start_replication(self)
277284

278-
for listener in listeners:
285+
for listener in self.config.server.listeners:
279286
if listener.type == "http":
280287
self._listening_services.extend(
281288
self._listener_http(self.config, listener)
@@ -407,7 +414,7 @@ async def start():
407414
# Loading the provider metadata also ensures the provider config is valid.
408415
await oidc.load_metadata()
409416

410-
await _base.start(hs, config.listeners)
417+
await _base.start(hs)
411418

412419
hs.get_datastore().db_pool.updates.start_doing_background_updates()
413420

synapse/config/logger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
)
3232

3333
import synapse
34-
from synapse.app import _base as appbase
3534
from synapse.logging._structured import setup_structured_logging
3635
from synapse.logging.context import LoggingContextFilter
3736
from synapse.logging.filter import MetadataFilter
@@ -318,6 +317,8 @@ def setup_logging(
318317
# Perform one-time logging configuration.
319318
_setup_stdlib_logging(config, log_config_path, logBeginner=logBeginner)
320319
# Add a SIGHUP handler to reload the logging configuration, if one is available.
320+
from synapse.app import _base as appbase
321+
321322
appbase.register_sighup(_reload_logging_config, log_config_path)
322323

323324
# Log immediately so we can grep backwards.

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: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import contextlib
1515
import logging
1616
import time
17-
from typing import Optional, Tuple, Type, Union
17+
from typing import Optional, Tuple, Union
1818

1919
import attr
2020
from zope.interface import implementer
@@ -50,6 +50,7 @@ class SynapseRequest(Request):
5050
* Redaction of access_token query-params in __repr__
5151
* Logging at start and end
5252
* Metrics to record CPU, wallclock and DB time by endpoint.
53+
* A limit to the size of request which will be accepted
5354
5455
It also provides a method `processing`, which returns a context manager. If this
5556
method is called, the request won't be logged until the context manager is closed;
@@ -60,8 +61,9 @@ class SynapseRequest(Request):
6061
logcontext: the log context for this request
6162
"""
6263

63-
def __init__(self, channel, *args, **kw):
64+
def __init__(self, channel, *args, max_request_body_size=1024, **kw):
6465
Request.__init__(self, channel, *args, **kw)
66+
self._max_request_body_size = max_request_body_size
6567
self.site = channel.site # type: SynapseSite
6668
self._channel = channel # this is used by the tests
6769
self.start_time = 0.0
@@ -98,6 +100,18 @@ def __repr__(self):
98100
self.site.site_tag,
99101
)
100102

103+
def handleContentChunk(self, data):
104+
# we should have a `content` by now.
105+
assert self.content, "handleContentChunk() called before gotLength()"
106+
if self.content.tell() + len(data) > self._max_request_body_size:
107+
logger.warning(
108+
"Aborting connection from %s because the request exceeds maximum size",
109+
self.client,
110+
)
111+
self.transport.abortConnection()
112+
return
113+
super().handleContentChunk(data)
114+
101115
@property
102116
def requester(self) -> Optional[Union[Requester, str]]:
103117
return self._requester
@@ -505,6 +519,7 @@ def __init__(
505519
config: ListenerConfig,
506520
resource: IResource,
507521
server_version_string,
522+
max_request_body_size: int,
508523
reactor: IReactorTime,
509524
):
510525
"""
@@ -516,6 +531,8 @@ def __init__(
516531
resource: The base of the resource tree to be used for serving requests on
517532
this site
518533
server_version_string: A string to present for the Server header
534+
max_request_body_size: Maximum request body length to allow before
535+
dropping the connection
519536
reactor: reactor to be used to manage connection timeouts
520537
"""
521538
Site.__init__(self, resource, reactor=reactor)
@@ -524,9 +541,14 @@ def __init__(
524541

525542
assert config.http_options is not None
526543
proxied = config.http_options.x_forwarded
527-
self.requestFactory = (
528-
XForwardedForRequest if proxied else SynapseRequest
529-
) # type: Type[Request]
544+
request_class = XForwardedForRequest if proxied else SynapseRequest
545+
546+
def request_factory(channel, queued) -> Request:
547+
return request_class(
548+
channel, max_request_body_size=max_request_body_size, queued=queued
549+
)
550+
551+
self.requestFactory = request_factory # type: ignore
530552
self.access_logger = logging.getLogger(logger_name)
531553
self.server_version_string = server_version_string.encode("ascii")
532554

synapse/rest/media/v1/upload_resource.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,6 @@ async def _async_render_OPTIONS(self, request: Request) -> None:
5151

5252
async def _async_render_POST(self, request: SynapseRequest) -> None:
5353
requester = await self.auth.get_user_by_req(request)
54-
# TODO: The checks here are a bit late. The content will have
55-
# already been uploaded to a tmp file at this point
5654
content_length = request.getHeader("Content-Length")
5755
if content_length is None:
5856
raise SynapseError(msg="Request must specify a Content-Length", code=400)

0 commit comments

Comments
 (0)