CVE-2021-29430: Allocation of Resources Without Limits or Throttling (CWE-770)
value: Sydent is a reference Matrix identity server. Sydent does not limit the size of requests it receives from HTTP clients. A malicious user could send an HTTP request with a very large body, leading to memory exhaustion and denial of service. Sydent also does not limit response size for requests it makes to remote Matrix homeservers. A malicious homeserver could return a very large response, again leading to memory exhaustion and denial of service. This affects any server which accepts registration requests from untrusted clients. This issue has been patched by releases 89071a1, 0523511, f56eee3. As a workaround request sizes can be limited in an HTTP reverse-proxy. There are no known workarounds for the problem with overlarge responses.
CWE description: The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
CVSS v3 base score: 7.5
Published: 2021-04-15T21:15Z
Repository: https://github.com/matrix-org/sydent
Language: Python
Files changed: sydent/hs_federation/verifier.py, sydent/http/httpclient.py, sydent/http/servlets/registerservlet.py
Commit date: 2021-03-17 12:45:31 -0400
Commit message: Add max size for other HTTP calls.

Diff: diff --git a/sydent/hs_federation/verifier.py b/sydent/hs_federation/verifier.py
index 78d9af0..1360672 100644
--- a/sydent/hs_federation/verifier.py
+++ b/sydent/hs_federation/verifier.py
@@ -67,11 +67,11 @@ class Verifier(object):
             now = int(time.time() * 1000)
             if cached['valid_until_ts'] > now:
                 defer.returnValue(self.cache[server_name]['verify_keys'])
 
         client = FederationHttpClient(self.sydent)
-        result = yield client.get_json("matrix://%s/_matrix/key/v2/server/" % server_name)
+        result = yield client.get_json("matrix://%s/_matrix/key/v2/server/" % server_name, 1024 * 50)
         if 'verify_keys' not in result:
             raise SignatureVerifyException("No key found in response")
 
         if 'valid_until_ts' in result:
             # Don't cache anything without a valid_until_ts or we wouldn't
diff --git a/sydent/http/httpclient.py b/sydent/http/httpclient.py
index 7358304..2da0c5f 100644
--- a/sydent/http/httpclient.py
+++ b/sydent/http/httpclient.py
@@ -23,35 +23,39 @@ from twisted.internet import defer
 from twisted.web.client import FileBodyProducer, Agent, readBody
 from twisted.web.http_headers import Headers
 from sydent.http.matrixfederationagent import MatrixFederationAgent
 
 from sydent.http.federation_tls_options import ClientTLSOptionsFactory
+from sydent.http.httpcommon import BodyExceededMaxSize, read_body_with_max_size
 
 logger = logging.getLogger(__name__)
 
 
 class HTTPClient(object):
     """A base HTTP class that contains methods for making GET and POST HTTP
     requests.
     """
     @defer.inlineCallbacks
-    def get_json(self, uri):
+    def get_json(self, uri, max_size = None):
         """Make a GET request to an endpoint returning JSON and parse result
 
         :param uri: The URI to make a GET request to.
         :type uri: unicode
 
+        :param max_size: The maximum size (in bytes) to allow as a response.
+        :type max_size: int
+
         :return: A deferred containing JSON parsed into a Python object.
         :rtype: twisted.internet.defer.Deferred[dict[any, any]]
         """
         logger.debug("HTTP GET %s", uri)
 
         response = yield self.agent.request(
             b"GET",
             uri.encode("utf8"),
         )
-        body = yield readBody(response)
+        body = yield read_body_with_max_size(response, max_size)
         try:
             # json.loads doesn't allow bytes in Python 3.5
             json_body = json.loads(body.decode("UTF-8"))
         except Exception as e:
             logger.exception("Error parsing JSON from %s", uri)
@@ -92,11 +96,15 @@ class HTTPClient(object):
         )
 
         # Ensure the body object is read otherwise we'll leak HTTP connections
         # as per
         # https://twistedmatrix.com/documents/current/web/howto/client.html
-        yield readBody(response)
+        try:
+            # TODO Will this cause the server to think the request was a failure?
+            yield read_body_with_max_size(response, 0)
+        except BodyExceededMaxSize:
+            pass
 
         defer.returnValue(response)
 
 class SimpleHttpClient(HTTPClient):
     """A simple, no-frills HTTP client based on the class of the same name
diff --git a/sydent/http/servlets/registerservlet.py b/sydent/http/servlets/registerservlet.py
index d385a47..ebd07cc 100644
--- a/sydent/http/servlets/registerservlet.py
+++ b/sydent/http/servlets/registerservlet.py
@@ -49,10 +49,11 @@ class RegisterServlet(Resource):
 
         result = yield self.client.get_json(
             "matrix://%s/_matrix/federation/v1/openid/userinfo?access_token=%s" % (
                 args['matrix_server_name'], urllib.parse.quote(args['access_token']),
             ),
+            1024 * 5,
         )
         if 'sub' not in result:
             raise Exception("Invalid response from homeserver")
 
         user_id = result['sub']

Vulnerable code:         result = yield client.get_json("matrix://%s/_matrix/key/v2/server/" % server_name)
    def get_json(self, uri):
        body = yield readBody(response)
        yield readBody(response)
Fixed code:         result = yield client.get_json("matrix://%s/_matrix/key/v2/server/" % server_name, 1024 * 50)
from sydent.http.httpcommon import BodyExceededMaxSize, read_body_with_max_size
    def get_json(self, uri, max_size = None):
        :param max_size: The maximum size (in bytes) to allow as a response.
        :type max_size: int

        body = yield read_body_with_max_size(response, max_size)
        try:
            # TODO Will this cause the server to think the request was a failure?
            yield read_body_with_max_size(response, 0)
        except BodyExceededMaxSize:
            pass
            1024 * 5,