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/http/httpcommon.py, sydent/http/httpserver.py
Commit date: 2021-04-06 14:10:08 +0000
Commit message: Limit maximum request size.

Request size shouldn't be unbounded because there is no need for it and
it can lead to DoS in some cases.

Diff: diff --git a/sydent/http/httpcommon.py b/sydent/http/httpcommon.py
index 9dde0ec..2088dd5 100644
--- a/sydent/http/httpcommon.py
+++ b/sydent/http/httpcommon.py
@@ -21,13 +21,19 @@ import twisted.internet.ssl
 from twisted.internet import defer, protocol
 from twisted.internet.protocol import connectionDone
 from twisted.web._newclient import ResponseDone
 from twisted.web.http import PotentialDataLoss
 from twisted.web.iweb import UNKNOWN_LENGTH
+from twisted.web import server
+
 
 logger = logging.getLogger(__name__)
 
+# Arbitrarily limited to 512 KiB.
+MAX_REQUEST_SIZE = 512 * 1024
+
+
 class SslComponents:
     def __init__(self, sydent):
         self.sydent = sydent
 
         self.myPrivateCertificate = self.makeMyCertificate()
@@ -59,20 +65,19 @@ class SslComponents:
         if len(caCertFilename) > 0:
             try:
                 fp = open(caCertFilename)
                 caCert = twisted.internet.ssl.Certificate.loadPEM(fp.read())
                 fp.close()
-            except:
+            except Exception:
                 logger.warn("Failed to open CA cert file %s", caCertFilename)
                 raise
             logger.warn("Using custom CA cert file: %s", caCertFilename)
             return twisted.internet._sslverify.OpenSSLCertificateAuthorities([caCert.original])
         else:
             return twisted.internet.ssl.OpenSSLDefaultPaths()
 
 
-
 class BodyExceededMaxSize(Exception):
     """The maximum allowed size of the HTTP body was exceeded."""
 
 
 class _DiscardBodyWithMaxSizeProtocol(protocol.Protocol):
@@ -121,11 +126,11 @@ class _ReadBodyWithMaxSizeProtocol(protocol.Protocol):
             self.deferred.errback(BodyExceededMaxSize())
             # Close the connection (forcefully) since all the data will get
             # discarded anyway.
             self.transport.abortConnection()
 
-    def connectionLost(self, reason = connectionDone) -> None:
+    def connectionLost(self, reason=connectionDone) -> None:
         # If the maximum size was already exceeded, there's nothing to do.
         if self.deferred.called:
             return
 
         if reason.check(ResponseDone):
@@ -161,5 +166,17 @@ def read_body_with_max_size(response, max_size):
             response.deliverBody(_DiscardBodyWithMaxSizeProtocol(d))
             return d
 
     response.deliverBody(_ReadBodyWithMaxSizeProtocol(d, max_size))
     return d
+
+
+class SizeLimitingRequest(server.Request):
+    def handleContentChunk(self, data):
+        if self.content.tell() + len(data) > MAX_REQUEST_SIZE:
+            logger.info(
+                "Aborting connection from %s because the request exceeds maximum size",
+                self.client.host)
+            self.transport.abortConnection()
+            return
+
+        return super().handleContentChunk(data)
diff --git a/sydent/http/httpserver.py b/sydent/http/httpserver.py
index 245f357..c40ef4c 100644
--- a/sydent/http/httpserver.py
+++ b/sydent/http/httpserver.py
@@ -27,10 +27,11 @@ from sydent.http.servlets.authenticated_bind_threepid_servlet import (
     AuthenticatedBindThreePidServlet,
 )
 from sydent.http.servlets.authenticated_unbind_threepid_servlet import (
     AuthenticatedUnbindThreePidServlet,
 )
+from sydent.http.httpcommon import SizeLimitingRequest
 
 logger = logging.getLogger(__name__)
 
 
 class ClientApiHttpServer:
@@ -127,10 +128,11 @@ class ClientApiHttpServer:
         v2.putChild(b'sign-ed25519', self.sydent.servlets.blindlySignStuffServletV2)
         v2.putChild(b'lookup', self.sydent.servlets.lookup_v2)
         v2.putChild(b'hash_details', self.sydent.servlets.hash_details)
 
         self.factory = Site(root)
+        self.factory.requestFactory = SizeLimitingRequest
         self.factory.displayTracebacks = False
 
     def setup(self):
         httpPort = int(self.sydent.cfg.get('http', 'clientapi.http.port'))
         interface = self.sydent.cfg.get('http', 'clientapi.http.bind_address')

Vulnerable code:             except:

    def connectionLost(self, reason = connectionDone) -> None:
Fixed code: from twisted.web import server

# Arbitrarily limited to 512 KiB.
MAX_REQUEST_SIZE = 512 * 1024


            except Exception:
    def connectionLost(self, reason=connectionDone) -> None:


class SizeLimitingRequest(server.Request):
    def handleContentChunk(self, data):
        if self.content.tell() + len(data) > MAX_REQUEST_SIZE:
            logger.info(
                "Aborting connection from %s because the request exceeds maximum size",
                self.client.host)
            self.transport.abortConnection()
            return

        return super().handleContentChunk(data)
from sydent.http.httpcommon import SizeLimitingRequest
        self.factory.requestFactory = SizeLimitingRequest