CVE-2021-29431: Server-Side Request Forgery (SSRF) (CWE-918)
value: Sydent is a reference Matrix identity server. Sydent can be induced to send HTTP GET requests to internal systems, due to lack of parameter validation or IP address blacklisting. It is not possible to exfiltrate data or control request headers, but it might be possible to use the attack to perform an internal port enumeration. This issue has been addressed in in 9e57334, 8936925, 3d531ed, 0f00412. A potential workaround would be to use a firewall to ensure that Sydent cannot reach internal HTTP resources.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 6.5
Published: 2021-04-15T21:15Z
Repository: https://github.com/matrix-org/sydent
Language: Python
Files changed: sydent/hs_federation/verifier.py, sydent/http/servlets/threepidunbindservlet.py, sydent/threepid/bind.py
Commit date: 2021-03-31 15:42:23 +0000
Commit message: Validate hostname in more places

Diff: diff --git a/sydent/hs_federation/verifier.py b/sydent/hs_federation/verifier.py
index 1360672..a708d40 100644
--- a/sydent/hs_federation/verifier.py
+++ b/sydent/hs_federation/verifier.py
@@ -23,10 +23,11 @@ from unpaddedbase64 import decode_base64
 import signedjson.sign
 import signedjson.key
 from signedjson.sign import SignatureVerifyException
 
 from sydent.http.httpclient import FederationHttpClient
+from sydent.util.stringutils import is_valid_hostname
 
 
 logger = logging.getLogger(__name__)
 
 
@@ -35,10 +36,17 @@ class NoAuthenticationError(Exception):
     Raised when no signature is provided that could be authenticated
     """
     pass
 
 
+class InvalidServerName(Exception):
+    """
+    Raised when the provided origin parameter is not a valid hostname (plus optional port).
+    """
+    pass
+
+
 class Verifier(object):
     """
     Verifies signed json blobs from Matrix Homeservers by finding the
     homeserver's address, contacting it, requesting its keys and
     verifying that the signature on the json blob matches.
@@ -195,10 +203,13 @@ class Verifier(object):
                 json_request["signatures"].setdefault(origin, {})[key] = sig
 
         if not json_request["signatures"]:
             raise NoAuthenticationError("Missing X-Matrix Authorization header")
 
+        if not is_valid_hostname(json_request["origin"]):
+            raise InvalidServerName("X-Matrix header's origin parameter must be a valid hostname")
+
         yield self.verifyServerSignedJson(json_request, [origin])
 
         logger.info("Verified request from HS %s", origin)
 
         defer.returnValue(origin)
diff --git a/sydent/http/servlets/threepidunbindservlet.py b/sydent/http/servlets/threepidunbindservlet.py
index ddc7db4..87fc616 100644
--- a/sydent/http/servlets/threepidunbindservlet.py
+++ b/sydent/http/servlets/threepidunbindservlet.py
@@ -17,11 +17,11 @@
 from __future__ import absolute_import
 
 import json
 import logging
 
-from sydent.hs_federation.verifier import NoAuthenticationError
+from sydent.hs_federation.verifier import NoAuthenticationError, InvalidServerName
 from signedjson.sign import SignatureVerifyException
 
 from sydent.http.servlets import dict_to_json_bytes
 from sydent.db.valsession import ThreePidValSessionStore
 from sydent.util.stringutils import is_valid_client_secret
@@ -79,11 +79,11 @@ class ThreePidUnbindServlet(Resource):
             # on the contents of the request. If the user has supplied "sid"
             # (the Session ID returned by Sydent during the original binding)
             # and "client_secret" fields, they are trying to prove that they
             # were the original author of the bind. We then check that what
             # they supply matches and if it does, allow the unbind.
-            # 
+            #
             # However if these fields are not supplied, we instead check
             # whether the request originated from a homeserver, and if so the
             # same homeserver that originally created the bind. We do this by
             # checking the signature of the request. If it all matches up, we
             # allow the unbind.
@@ -119,11 +119,11 @@ class ThreePidUnbindServlet(Resource):
                     request.write(dict_to_json_bytes({
                         'errcode': 'M_SESSION_NOT_VALIDATED',
                         'error': "This validation session has not yet been completed"
                     }))
                     return
-                
+
                 if s.medium != threepid['medium'] or s.address != threepid['address']:
                     request.setResponseCode(403)
                     request.write(dict_to_json_bytes({
                         'errcode': 'M_FORBIDDEN',
                         'error': 'Provided session information does not match medium/address combo',
@@ -141,11 +141,16 @@ class ThreePidUnbindServlet(Resource):
                 except NoAuthenticationError as ex:
                     request.setResponseCode(401)
                     request.write(dict_to_json_bytes({'errcode': 'M_FORBIDDEN', 'error': str(ex)}))
                     request.finish()
                     return
-                except:
+                except InvalidServerName as ex:
+                    request.setResponseCode(400)
+                    request.write(dict_to_json_bytes({'errcode': 'M_INVALID_PARAM', 'error': str(ex)}))
+                    request.finish()
+                    return
+                except Exception:
                     logger.exception("Exception whilst authenticating unbind request")
                     request.setResponseCode(500)
                     request.write(dict_to_json_bytes({'errcode': 'M_UNKNOWN', 'error': 'Internal Server Error'}))
                     request.finish()
                     return
diff --git a/sydent/threepid/bind.py b/sydent/threepid/bind.py
index a331746..84160bf 100644
--- a/sydent/threepid/bind.py
+++ b/sydent/threepid/bind.py
@@ -30,10 +30,12 @@ from sydent.db.hashing_metadata import HashingMetadataStore
 from sydent.threepid.signer import Signer
 from sydent.http.httpclient import FederationHttpClient
 
 from sydent.threepid import ThreepidAssociation
 
+from sydent.util.stringutils import is_valid_hostname
+
 from twisted.internet import defer
 
 logger = logging.getLogger(__name__)
 
 
@@ -129,19 +131,29 @@ class ThreepidBinder:
         :param attempt: The number of previous attempts to send this association.
         :type attempt: int
         """
         mxid = assoc["mxid"]
         mxid_parts = mxid.split(":", 1)
+
         if len(mxid_parts) != 2:
             logger.error(
                 "Can't notify on bind for unparseable mxid %s. Not retrying.",
                 assoc["mxid"],
             )
             return
 
+        matrix_server = mxid_parts[1]
+
+        if not is_valid_hostname(matrix_server):
+            logger.error(
+                "MXID server part '%s' not a valid hostname. Not retrying.",
+                matrix_server,
+            )
+            return
+
         post_url = "matrix://%s/_matrix/federation/v1/3pid/onbind" % (
-            mxid_parts[1],
+            matrix_server,
         )
 
         logger.info("Making bind callback to: %s", post_url)
 
         # Make a POST to the chosen Synapse server

Vulnerable code: from sydent.hs_federation.verifier import NoAuthenticationError
            # 
                
                except:
            mxid_parts[1],
Fixed code: from sydent.util.stringutils import is_valid_hostname
class InvalidServerName(Exception):
    """
    Raised when the provided origin parameter is not a valid hostname (plus optional port).
    """
    pass


        if not is_valid_hostname(json_request["origin"]):
            raise InvalidServerName("X-Matrix header's origin parameter must be a valid hostname")

from sydent.hs_federation.verifier import NoAuthenticationError, InvalidServerName
            #

                except InvalidServerName as ex:
                    request.setResponseCode(400)
                    request.write(dict_to_json_bytes({'errcode': 'M_INVALID_PARAM', 'error': str(ex)}))
                    request.finish()
                    return
                except Exception:
from sydent.util.stringutils import is_valid_hostname


        matrix_server = mxid_parts[1]

        if not is_valid_hostname(matrix_server):
            logger.error(
                "MXID server part '%s' not a valid hostname. Not retrying.",
                matrix_server,
            )
            return

            matrix_server,