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

Commit 58a2b3b

Browse files
committed
Correctly check the size of uploaded files
1 parent f23c0bd commit 58a2b3b

File tree

12 files changed

+193
-36
lines changed

12 files changed

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +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
36+
from synapse.config.homeserver import HomeServerConfig
3537
from synapse.crypto import context_factory
3638
from synapse.logging.context import PreserveLoggingContext
3739
from synapse.metrics.background_process_metrics import wrap_as_background_process
@@ -528,3 +530,25 @@ def sdnotify(state):
528530
# this is a bit surprising, since we don't expect to have a NOTIFY_SOCKET
529531
# unless systemd is expecting us to notify it.
530532
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 65535 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/generic_worker.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
@@ -390,6 +390,7 @@ def _listen_http(self, listener_config: ListenerConfig):
390390
listener_config,
391391
root_resource,
392392
self.version_string,
393+
max_request_body_size=max_request_body_size(self.config),
393394
),
394395
reactor=self.get_reactor(),
395396
)

synapse/app/homeserver.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -126,19 +132,21 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf
126132
else:
127133
root_resource = OptionsResource()
128134

129-
root_resource = create_resource_tree(resources, root_resource)
135+
site = SynapseSite(
136+
"synapse.access.%s.%s" % ("https" if tls else "http", site_tag),
137+
site_tag,
138+
listener_config,
139+
create_resource_tree(resources, root_resource),
140+
self.version_string,
141+
max_request_body_size=max_request_body_size(self.config),
142+
reactor=self.get_reactor(),
143+
)
130144

131145
if tls:
132146
ports = listen_ssl(
133147
bind_addresses,
134148
port,
135-
SynapseSite(
136-
"synapse.access.https.%s" % (site_tag,),
137-
site_tag,
138-
listener_config,
139-
root_resource,
140-
self.version_string,
141-
),
149+
site,
142150
self.tls_server_context_factory,
143151
reactor=self.get_reactor(),
144152
)
@@ -148,13 +156,7 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf
148156
ports = listen_tcp(
149157
bind_addresses,
150158
port,
151-
SynapseSite(
152-
"synapse.access.http.%s" % (site_tag,),
153-
site_tag,
154-
listener_config,
155-
root_resource,
156-
self.version_string,
157-
),
159+
site,
158160
reactor=self.get_reactor(),
159161
)
160162
logger.info("Synapse now listening on TCP port %d", port)

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: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@
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
2121

22-
from twisted.internet.interfaces import IAddress
22+
from twisted.internet.interfaces import IAddress, IReactorTime
2323
from twisted.python.failure import Failure
24+
from twisted.web.resource import IResource
2425
from twisted.web.server import Request, Site
2526

2627
from synapse.config.server import ListenerConfig
@@ -49,6 +50,7 @@ class SynapseRequest(Request):
4950
* Redaction of access_token query-params in __repr__
5051
* Logging at start and end
5152
* Metrics to record CPU, wallclock and DB time by endpoint.
53+
* A limit to the size of request which will be accepted
5254
5355
It also provides a method `processing`, which returns a context manager. If this
5456
method is called, the request won't be logged until the context manager is closed;
@@ -59,8 +61,9 @@ class SynapseRequest(Request):
5961
logcontext: the log context for this request
6062
"""
6163

62-
def __init__(self, channel, *args, **kw):
64+
def __init__(self, channel, *args, max_request_body_size=1024, **kw):
6365
Request.__init__(self, channel, *args, **kw)
66+
self._max_request_body_size = max_request_body_size
6467
self.site = channel.site # type: SynapseSite
6568
self._channel = channel # this is used by the tests
6669
self.start_time = 0.0
@@ -97,6 +100,18 @@ def __repr__(self):
97100
self.site.site_tag,
98101
)
99102

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+
100115
@property
101116
def requester(self) -> Optional[Union[Requester, str]]:
102117
return self._requester
@@ -485,29 +500,55 @@ class _XForwardedForAddress:
485500

486501
class SynapseSite(Site):
487502
"""
488-
Subclass of a twisted http Site that does access logging with python's
489-
standard logging
503+
Synapse-specific twisted http Site
504+
505+
This does two main things.
506+
507+
First, it replaces the requestFactory in use so that we build SynapseRequests
508+
instead of regular t.w.server.Requests. All of the constructor params are really
509+
just parameters for SynapseRequest.
510+
511+
Second, it inhibits the log() method called by Request.finish, since SynapseRequest
512+
does its own logging.
490513
"""
491514

492515
def __init__(
493516
self,
494-
logger_name,
495-
site_tag,
517+
logger_name: str,
518+
site_tag: str,
496519
config: ListenerConfig,
497-
resource,
520+
resource: IResource,
498521
server_version_string,
499-
*args,
500-
**kwargs,
522+
max_request_body_size: int,
523+
reactor: Optional[IReactorTime] = None,
501524
):
502-
Site.__init__(self, resource, *args, **kwargs)
525+
"""
526+
527+
Args:
528+
logger_name: The name of the logger to use for access logs.
529+
site_tag: A tag to use for this site - mostly in access logs.
530+
config: Configuration for the HTTP listener corresponding to this site
531+
resource: The base of the resource tree to be used for serving requests on
532+
this site
533+
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
536+
reactor: reactor to be used to manage connection timeouts
537+
"""
538+
Site.__init__(self, resource, reactor=reactor)
503539

504540
self.site_tag = site_tag
505541

506542
assert config.http_options is not None
507543
proxied = config.http_options.x_forwarded
508-
self.requestFactory = (
509-
XForwardedForRequest if proxied else SynapseRequest
510-
) # 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
511552
self.access_logger = logging.getLogger(logger_name)
512553
self.server_version_string = server_version_string.encode("ascii")
513554

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)

tests/http/test_site.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
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+
# we start with the chunk size, in hex. (We won't actually send this much)
74+
protocol.dataReceived(b"10000000\r\n")
75+
sent = 0
76+
while not transport.disconnected:
77+
self.assertLess(sent, 0x10000000, "connection did not drop")
78+
protocol.dataReceived(b"\0" * 1024)
79+
sent += 1024
80+
81+
# default max upload size is 50M, so it should drop on the next buffer after
82+
# that.
83+
self.assertEqual(sent, 50 * 1024 * 1024 + 1024)

tests/replication/_base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,7 @@ def make_worker_hs(
349349
config=worker_hs.config.server.listeners[0],
350350
resource=resource,
351351
server_version_string="1",
352+
max_request_body_size=1234,
352353
)
353354

354355
if worker_hs.config.redis.redis_enabled:

0 commit comments

Comments
 (0)