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

Commit

Permalink
Improved validation for received requests (#9817)
Browse files Browse the repository at this point in the history
* Simplify `start_listening` callpath

* Correctly check the size of uploaded files
  • Loading branch information
richvdh authored Apr 23, 2021
1 parent 84936e2 commit 3ff2251
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 31 deletions.
1 change: 1 addition & 0 deletions changelog.d/9817.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a long-standing bug which caused `max_upload_size` to not be correctly enforced.
3 changes: 3 additions & 0 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

"""Contains constants from the specification."""

# the max size of a (canonical-json-encoded) event
MAX_PDU_SIZE = 65536

# the "depth" field on events is limited to 2**63 - 1
MAX_DEPTH = 2 ** 63 - 1

Expand Down
30 changes: 26 additions & 4 deletions synapse/app/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@
from twisted.protocols.tls import TLSMemoryBIOFactory

import synapse
from synapse.api.constants import MAX_PDU_SIZE
from synapse.app import check_bind_error
from synapse.app.phone_stats_home import start_phone_stats_home
from synapse.config.server import ListenerConfig
from synapse.config.homeserver import HomeServerConfig
from synapse.crypto import context_factory
from synapse.logging.context import PreserveLoggingContext
from synapse.metrics.background_process_metrics import wrap_as_background_process
Expand Down Expand Up @@ -288,7 +289,7 @@ def refresh_certificate(hs):
logger.info("Context factories updated.")


async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerConfig]):
async def start(hs: "synapse.server.HomeServer"):
"""
Start a Synapse server or worker.
Expand All @@ -300,7 +301,6 @@ async def start(hs: "synapse.server.HomeServer", listeners: Iterable[ListenerCon
Args:
hs: homeserver instance
listeners: Listener configuration ('listeners' in homeserver.yaml)
"""
# Set up the SIGHUP machinery.
if hasattr(signal, "SIGHUP"):
Expand Down Expand Up @@ -336,7 +336,7 @@ def run_sighup(*args, **kwargs):
synapse.logging.opentracing.init_tracer(hs) # type: ignore[attr-defined] # noqa

# It is now safe to start your Synapse.
hs.start_listening(listeners)
hs.start_listening()
hs.get_datastore().db_pool.start_profiling()
hs.get_pusherpool().start()

Expand Down Expand Up @@ -530,3 +530,25 @@ def sdnotify(state):
# this is a bit surprising, since we don't expect to have a NOTIFY_SOCKET
# unless systemd is expecting us to notify it.
logger.warning("Unable to send notification to systemd: %s", e)


def max_request_body_size(config: HomeServerConfig) -> int:
"""Get a suitable maximum size for incoming HTTP requests"""

# Other than media uploads, the biggest request we expect to see is a fully-loaded
# /federation/v1/send request.
#
# The main thing in such a request is up to 50 PDUs, and up to 100 EDUs. PDUs are
# limited to 65536 bytes (possibly slightly more if the sender didn't use canonical
# json encoding); there is no specced limit to EDUs (see
# https://github.com/matrix-org/matrix-doc/issues/3121).
#
# in short, we somewhat arbitrarily limit requests to 200 * 64K (about 12.5M)
#
max_request_size = 200 * MAX_PDU_SIZE

# if we have a media repo enabled, we may need to allow larger uploads than that
if config.media.can_load_media_repo:
max_request_size = max(max_request_size, config.media.max_upload_size)

return max_request_size
8 changes: 1 addition & 7 deletions synapse/app/admin_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ class AdminCmdSlavedStore(
class AdminCmdServer(HomeServer):
DATASTORE_CLASS = AdminCmdSlavedStore

def _listen_http(self, listener_config):
pass

def start_listening(self, listeners):
pass


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

async def run():
with LoggingContext("command"):
_base.start(ss, [])
_base.start(ss)
await args.func(ss, args)

_base.start_worker_reactor(
Expand Down
11 changes: 6 additions & 5 deletions synapse/app/generic_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# limitations under the License.
import logging
import sys
from typing import Dict, Iterable, Optional
from typing import Dict, Optional

from twisted.internet import address
from twisted.web.resource import IResource
Expand All @@ -32,7 +32,7 @@
SERVER_KEY_V2_PREFIX,
)
from synapse.app import _base
from synapse.app._base import register_start
from synapse.app._base import max_request_body_size, register_start
from synapse.config._base import ConfigError
from synapse.config.homeserver import HomeServerConfig
from synapse.config.logger import setup_logging
Expand Down Expand Up @@ -367,15 +367,16 @@ def _listen_http(self, listener_config: ListenerConfig):
listener_config,
root_resource,
self.version_string,
max_request_body_size=max_request_body_size(self.config),
reactor=self.get_reactor(),
),
reactor=self.get_reactor(),
)

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

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

register_start(_base.start, hs, config.worker_listeners)
register_start(_base.start, hs)

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

Expand Down
17 changes: 12 additions & 5 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import logging
import os
import sys
from typing import Iterable, Iterator
from typing import Iterator

from twisted.internet import reactor
from twisted.web.resource import EncodingResourceWrapper, IResource
Expand All @@ -36,7 +36,13 @@
WEB_CLIENT_PREFIX,
)
from synapse.app import _base
from synapse.app._base import listen_ssl, listen_tcp, quit_with_error, register_start
from synapse.app._base import (
listen_ssl,
listen_tcp,
max_request_body_size,
quit_with_error,
register_start,
)
from synapse.config._base import ConfigError
from synapse.config.emailconfig import ThreepidBehaviour
from synapse.config.homeserver import HomeServerConfig
Expand Down Expand Up @@ -132,6 +138,7 @@ def _listener_http(self, config: HomeServerConfig, listener_config: ListenerConf
listener_config,
create_resource_tree(resources, root_resource),
self.version_string,
max_request_body_size=max_request_body_size(self.config),
reactor=self.get_reactor(),
)

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

return resources

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

for listener in listeners:
for listener in self.config.server.listeners:
if listener.type == "http":
self._listening_services.extend(
self._listener_http(self.config, listener)
Expand Down Expand Up @@ -407,7 +414,7 @@ async def start():
# Loading the provider metadata also ensures the provider config is valid.
await oidc.load_metadata()

await _base.start(hs, config.listeners)
await _base.start(hs)

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

Expand Down
3 changes: 2 additions & 1 deletion synapse/config/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
)

import synapse
from synapse.app import _base as appbase
from synapse.logging._structured import setup_structured_logging
from synapse.logging.context import LoggingContextFilter
from synapse.logging.filter import MetadataFilter
Expand Down Expand Up @@ -318,6 +317,8 @@ def setup_logging(
# Perform one-time logging configuration.
_setup_stdlib_logging(config, log_config_path, logBeginner=logBeginner)
# Add a SIGHUP handler to reload the logging configuration, if one is available.
from synapse.app import _base as appbase

appbase.register_sighup(_reload_logging_config, log_config_path)

# Log immediately so we can grep backwards.
Expand Down
4 changes: 2 additions & 2 deletions synapse/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from signedjson.sign import SignatureVerifyException, verify_signed_json
from unpaddedbase64 import decode_base64

from synapse.api.constants import EventTypes, JoinRules, Membership
from synapse.api.constants import MAX_PDU_SIZE, EventTypes, JoinRules, Membership
from synapse.api.errors import AuthError, EventSizeError, SynapseError
from synapse.api.room_versions import (
KNOWN_ROOM_VERSIONS,
Expand Down Expand Up @@ -205,7 +205,7 @@ def too_big(field):
too_big("type")
if len(event.event_id) > 255:
too_big("event_id")
if len(encode_canonical_json(event.get_pdu_json())) > 65536:
if len(encode_canonical_json(event.get_pdu_json())) > MAX_PDU_SIZE:
too_big("event")


Expand Down
32 changes: 27 additions & 5 deletions synapse/http/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import contextlib
import logging
import time
from typing import Optional, Tuple, Type, Union
from typing import Optional, Tuple, Union

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

def __init__(self, channel, *args, **kw):
def __init__(self, channel, *args, max_request_body_size=1024, **kw):
Request.__init__(self, channel, *args, **kw)
self._max_request_body_size = max_request_body_size
self.site = channel.site # type: SynapseSite
self._channel = channel # this is used by the tests
self.start_time = 0.0
Expand Down Expand Up @@ -98,6 +100,18 @@ def __repr__(self):
self.site.site_tag,
)

def handleContentChunk(self, data):
# we should have a `content` by now.
assert self.content, "handleContentChunk() called before gotLength()"
if self.content.tell() + len(data) > self._max_request_body_size:
logger.warning(
"Aborting connection from %s because the request exceeds maximum size",
self.client,
)
self.transport.abortConnection()
return
super().handleContentChunk(data)

@property
def requester(self) -> Optional[Union[Requester, str]]:
return self._requester
Expand Down Expand Up @@ -505,6 +519,7 @@ def __init__(
config: ListenerConfig,
resource: IResource,
server_version_string,
max_request_body_size: int,
reactor: IReactorTime,
):
"""
Expand All @@ -516,6 +531,8 @@ def __init__(
resource: The base of the resource tree to be used for serving requests on
this site
server_version_string: A string to present for the Server header
max_request_body_size: Maximum request body length to allow before
dropping the connection
reactor: reactor to be used to manage connection timeouts
"""
Site.__init__(self, resource, reactor=reactor)
Expand All @@ -524,9 +541,14 @@ def __init__(

assert config.http_options is not None
proxied = config.http_options.x_forwarded
self.requestFactory = (
XForwardedForRequest if proxied else SynapseRequest
) # type: Type[Request]
request_class = XForwardedForRequest if proxied else SynapseRequest

def request_factory(channel, queued) -> Request:
return request_class(
channel, max_request_body_size=max_request_body_size, queued=queued
)

self.requestFactory = request_factory # type: ignore
self.access_logger = logging.getLogger(logger_name)
self.server_version_string = server_version_string.encode("ascii")

Expand Down
2 changes: 0 additions & 2 deletions synapse/rest/media/v1/upload_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ async def _async_render_OPTIONS(self, request: Request) -> None:

async def _async_render_POST(self, request: SynapseRequest) -> None:
requester = await self.auth.get_user_by_req(request)
# TODO: The checks here are a bit late. The content will have
# already been uploaded to a tmp file at this point
content_length = request.getHeader("Content-Length")
if content_length is None:
raise SynapseError(msg="Request must specify a Content-Length", code=400)
Expand Down
8 changes: 8 additions & 0 deletions synapse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,14 @@ def setup(self) -> None:
if self.config.run_background_tasks:
self.setup_background_tasks()

def start_listening(self) -> None:
"""Start the HTTP, manhole, metrics, etc listeners
Does nothing in this base class; overridden in derived classes to start the
appropriate listeners.
"""
pass

def setup_background_tasks(self) -> None:
"""
Some handlers have side effects on instantiation (like registering
Expand Down
Loading

0 comments on commit 3ff2251

Please sign in to comment.