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

Commit ff5c4da

Browse files
authored
Add a maximum size for well-known lookups. (#8950)
1 parent e1b8e37 commit ff5c4da

File tree

5 files changed

+80
-18
lines changed

5 files changed

+80
-18
lines changed

changelog.d/8950.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a maximum size of 50 kilobytes to .well-known lookups.

synapse/http/client.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -720,11 +720,14 @@ async def get_file(
720720

721721
try:
722722
length = await make_deferred_yieldable(
723-
readBodyToFile(response, output_stream, max_size)
723+
read_body_with_max_size(response, output_stream, max_size)
724+
)
725+
except BodyExceededMaxSize:
726+
SynapseError(
727+
502,
728+
"Requested file is too large > %r bytes" % (max_size,),
729+
Codes.TOO_LARGE,
724730
)
725-
except SynapseError:
726-
# This can happen e.g. because the body is too large.
727-
raise
728731
except Exception as e:
729732
raise SynapseError(502, ("Failed to download remote body: %s" % e)) from e
730733

@@ -748,7 +751,11 @@ def _timeout_to_request_timed_out_error(f: Failure):
748751
return f
749752

750753

751-
class _ReadBodyToFileProtocol(protocol.Protocol):
754+
class BodyExceededMaxSize(Exception):
755+
"""The maximum allowed size of the HTTP body was exceeded."""
756+
757+
758+
class _ReadBodyWithMaxSizeProtocol(protocol.Protocol):
752759
def __init__(
753760
self, stream: BinaryIO, deferred: defer.Deferred, max_size: Optional[int]
754761
):
@@ -761,13 +768,7 @@ def dataReceived(self, data: bytes) -> None:
761768
self.stream.write(data)
762769
self.length += len(data)
763770
if self.max_size is not None and self.length >= self.max_size:
764-
self.deferred.errback(
765-
SynapseError(
766-
502,
767-
"Requested file is too large > %r bytes" % (self.max_size,),
768-
Codes.TOO_LARGE,
769-
)
770-
)
771+
self.deferred.errback(BodyExceededMaxSize())
771772
self.deferred = defer.Deferred()
772773
self.transport.loseConnection()
773774

@@ -782,12 +783,15 @@ def connectionLost(self, reason: Failure) -> None:
782783
self.deferred.errback(reason)
783784

784785

785-
def readBodyToFile(
786+
def read_body_with_max_size(
786787
response: IResponse, stream: BinaryIO, max_size: Optional[int]
787788
) -> defer.Deferred:
788789
"""
789790
Read a HTTP response body to a file-object. Optionally enforcing a maximum file size.
790791
792+
If the maximum file size is reached, the returned Deferred will resolve to a
793+
Failure with a BodyExceededMaxSize exception.
794+
791795
Args:
792796
response: The HTTP response to read from.
793797
stream: The file-object to write to.
@@ -798,7 +802,7 @@ def readBodyToFile(
798802
"""
799803

800804
d = defer.Deferred()
801-
response.deliverBody(_ReadBodyToFileProtocol(stream, d, max_size))
805+
response.deliverBody(_ReadBodyWithMaxSizeProtocol(stream, d, max_size))
802806
return d
803807

804808

synapse/http/federation/well_known_resolver.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@
1515
import logging
1616
import random
1717
import time
18+
from io import BytesIO
1819
from typing import Callable, Dict, Optional, Tuple
1920

2021
import attr
2122

2223
from twisted.internet import defer
2324
from twisted.internet.interfaces import IReactorTime
24-
from twisted.web.client import RedirectAgent, readBody
25+
from twisted.web.client import RedirectAgent
2526
from twisted.web.http import stringToDatetime
2627
from twisted.web.http_headers import Headers
2728
from twisted.web.iweb import IAgent, IResponse
2829

30+
from synapse.http.client import BodyExceededMaxSize, read_body_with_max_size
2931
from synapse.logging.context import make_deferred_yieldable
3032
from synapse.util import Clock, json_decoder
3133
from synapse.util.caches.ttlcache import TTLCache
@@ -53,6 +55,9 @@
5355
# lower bound for .well-known cache period
5456
WELL_KNOWN_MIN_CACHE_PERIOD = 5 * 60
5557

58+
# The maximum size (in bytes) to allow a well-known file to be.
59+
WELL_KNOWN_MAX_SIZE = 50 * 1024 # 50 KiB
60+
5661
# Attempt to refetch a cached well-known N% of the TTL before it expires.
5762
# e.g. if set to 0.2 and we have a cached entry with a TTL of 5mins, then
5863
# we'll start trying to refetch 1 minute before it expires.
@@ -229,6 +234,9 @@ async def _make_well_known_request(
229234
server_name: name of the server, from the requested url
230235
retry: Whether to retry the request if it fails.
231236
237+
Raises:
238+
_FetchWellKnownFailure if we fail to lookup a result
239+
232240
Returns:
233241
Returns the response object and body. Response may be a non-200 response.
234242
"""
@@ -250,7 +258,11 @@ async def _make_well_known_request(
250258
b"GET", uri, headers=Headers(headers)
251259
)
252260
)
253-
body = await make_deferred_yieldable(readBody(response))
261+
body_stream = BytesIO()
262+
await make_deferred_yieldable(
263+
read_body_with_max_size(response, body_stream, WELL_KNOWN_MAX_SIZE)
264+
)
265+
body = body_stream.getvalue()
254266

255267
if 500 <= response.code < 600:
256268
raise Exception("Non-200 response %s" % (response.code,))
@@ -259,6 +271,15 @@ async def _make_well_known_request(
259271
except defer.CancelledError:
260272
# Bail if we've been cancelled
261273
raise
274+
except BodyExceededMaxSize:
275+
# If the well-known file was too large, do not keep attempting
276+
# to download it, but consider it a temporary error.
277+
logger.warning(
278+
"Requested .well-known file for %s is too large > %r bytes",
279+
server_name.decode("ascii"),
280+
WELL_KNOWN_MAX_SIZE,
281+
)
282+
raise _FetchWellKnownFailure(temporary=True)
262283
except Exception as e:
263284
if not retry or i >= WELL_KNOWN_RETRY_ATTEMPTS:
264285
logger.info("Error fetching %s: %s", uri_str, e)

synapse/http/matrixfederationclient.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@
3737
import synapse.metrics
3838
import synapse.util.retryutils
3939
from synapse.api.errors import (
40+
Codes,
4041
FederationDeniedError,
4142
HttpResponseException,
4243
RequestSendFailed,
44+
SynapseError,
4345
)
4446
from synapse.http import QuieterFileBodyProducer
4547
from synapse.http.client import (
4648
BlacklistingAgentWrapper,
4749
BlacklistingReactorWrapper,
50+
BodyExceededMaxSize,
4851
encode_query_args,
49-
readBodyToFile,
52+
read_body_with_max_size,
5053
)
5154
from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent
5255
from synapse.logging.context import make_deferred_yieldable
@@ -975,9 +978,15 @@ async def get_file(
975978
headers = dict(response.headers.getAllRawHeaders())
976979

977980
try:
978-
d = readBodyToFile(response, output_stream, max_size)
981+
d = read_body_with_max_size(response, output_stream, max_size)
979982
d.addTimeout(self.default_timeout, self.reactor)
980983
length = await make_deferred_yieldable(d)
984+
except BodyExceededMaxSize:
985+
msg = "Requested file is too large > %r bytes" % (max_size,)
986+
logger.warning(
987+
"{%s} [%s] %s", request.txn_id, request.destination, msg,
988+
)
989+
SynapseError(502, msg, Codes.TOO_LARGE)
981990
except Exception as e:
982991
logger.warning(
983992
"{%s} [%s] Error reading response: %s",

tests/http/federation/test_matrix_federation_agent.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from synapse.http.federation.matrix_federation_agent import MatrixFederationAgent
3737
from synapse.http.federation.srv_resolver import Server
3838
from synapse.http.federation.well_known_resolver import (
39+
WELL_KNOWN_MAX_SIZE,
3940
WellKnownResolver,
4041
_cache_period_from_headers,
4142
)
@@ -1107,6 +1108,32 @@ def test_well_known_cache_with_temp_failure(self):
11071108
r = self.successResultOf(fetch_d)
11081109
self.assertEqual(r.delegated_server, None)
11091110

1111+
def test_well_known_too_large(self):
1112+
"""A well-known query that returns a result which is too large should be rejected."""
1113+
self.reactor.lookups["testserv"] = "1.2.3.4"
1114+
1115+
fetch_d = defer.ensureDeferred(
1116+
self.well_known_resolver.get_well_known(b"testserv")
1117+
)
1118+
1119+
# there should be an attempt to connect on port 443 for the .well-known
1120+
clients = self.reactor.tcpClients
1121+
self.assertEqual(len(clients), 1)
1122+
(host, port, client_factory, _timeout, _bindAddress) = clients.pop(0)
1123+
self.assertEqual(host, "1.2.3.4")
1124+
self.assertEqual(port, 443)
1125+
1126+
self._handle_well_known_connection(
1127+
client_factory,
1128+
expected_sni=b"testserv",
1129+
response_headers={b"Cache-Control": b"max-age=1000"},
1130+
content=b'{ "m.server": "' + (b"a" * WELL_KNOWN_MAX_SIZE) + b'" }',
1131+
)
1132+
1133+
# The result is sucessful, but disabled delegation.
1134+
r = self.successResultOf(fetch_d)
1135+
self.assertIsNone(r.delegated_server)
1136+
11101137
def test_srv_fallbacks(self):
11111138
"""Test that other SRV results are tried if the first one fails.
11121139
"""

0 commit comments

Comments
 (0)