Vulnerability: util/emailutils.py in Matrix Sydent before 1.0.2 mishandles registration restrictions that are based on e-mail domain, if the allowed_local_3pids option is enabled. This occurs because of potentially unwanted behavior in Python, in which an email.utils.parseaddr call on user@bad.example.net@good.example.com returns the user@bad.example.net substring.
Vulnerability ID: CVE-2019-11340
CWE: CWE-20
Severity score: 2.9
Project: https://github.com/matrix-org/sydent
Published: 2019-04-19
File: sydent/util/emailutils.py
Commit message: Require that parsed mail addresses match raw input

Python's email parser turns malformed addresses into valid ones
by removing anything after and including the second '@'. This
meant that sydent sent emails to the validated address but stored
the raw address as validated.

This rejects any requests where the parsed email address does not
match the raw input.
Commit author: David Baker

Vulnerable version (before fix):
 def sendEmail(sydent, templateName, mailTo, substitutions):             allSubstitutions[k+"_forhtml"] = cgi.escape(v.decode('utf8'))             allSubstitutions[k+"_forurl"] = urllib.quote(v)          mailString = open(mailTemplateFile).read() % allSubstitutions         rawFrom = email.utils.parseaddr(mailFrom)[1]         rawTo = email.utils.parseaddr(mailTo)[1]         if rawFrom == '' or rawTo == '':             logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)             raise EmailAddressException()         mailServer = sydent.cfg.get('email', 'email.smtphost')         mailPort = sydent.cfg.get('email', 'email.smtpport')         mailUsername = sydent.cfg.get('email', 'email.smtpusername') def sendEmail(sydent, templateName, mailTo, substitutions):                 smtp = smtplib.SMTP(mailServer, mailPort, myHostname)             if mailUsername != '':                 smtp.login(mailUsername, mailPassword)             smtp.sendmail(rawFrom, rawTo, mailString.encode('utf-8'))             smtp.quit()         except Exception as origException:             twisted.python.log.err()

Fixed version (after fix):
 def sendEmail(sydent, templateName, mailTo, substitutions):             allSubstitutions[k+"_forhtml"] = cgi.escape(v.decode('utf8'))             allSubstitutions[k+"_forurl"] = urllib.quote(v)          mailString = open(mailTemplateFile).read().decode('utf8') % allSubstitutions         parsedFrom = email.utils.parseaddr(mailFrom)[1]         parsedTo = email.utils.parseaddr(mailTo)[1]         if parsedFrom == '' or parsedTo == '':             logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)             raise EmailAddressException()          mailServer = sydent.cfg.get('email', 'email.smtphost')         mailPort = sydent.cfg.get('email', 'email.smtpport')         mailUsername = sydent.cfg.get('email', 'email.smtpusername') def sendEmail(sydent, templateName, mailTo, substitutions):                 smtp = smtplib.SMTP(mailServer, mailPort, myHostname)             if mailUsername != '':                 smtp.login(mailUsername, mailPassword)              # We're using the parsing above to do basic validation, but instead of             # failing it may munge the address it returns. So we should *not* use             # that parsed address, as it may not match any validation done             # elsewhere.             smtp.sendmail(mailFrom, mailTo, mailString.encode('utf-8'))             smtp.quit()         except Exception as origException:             twisted.python.log.err()

Patch:
patch: @@ -55,12 +55,13 @@ def sendEmail(sydent, templateName, mailTo, substitutions):
             allSubstitutions[k+"_forhtml"] = cgi.escape(v.decode('utf8'))
             allSubstitutions[k+"_forurl"] = urllib.quote(v)
 
-        mailString = open(mailTemplateFile).read() % allSubstitutions
-        rawFrom = email.utils.parseaddr(mailFrom)[1]
-        rawTo = email.utils.parseaddr(mailTo)[1]
-        if rawFrom == '' or rawTo == '':
+        mailString = open(mailTemplateFile).read().decode('utf8') % allSubstitutions
+        parsedFrom = email.utils.parseaddr(mailFrom)[1]
+        parsedTo = email.utils.parseaddr(mailTo)[1]
+        if parsedFrom == '' or parsedTo == '':
             logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)
             raise EmailAddressException()
+
         mailServer = sydent.cfg.get('email', 'email.smtphost')
         mailPort = sydent.cfg.get('email', 'email.smtpport')
         mailUsername = sydent.cfg.get('email', 'email.smtpusername')
@@ -77,7 +78,12 @@ def sendEmail(sydent, templateName, mailTo, substitutions):
                 smtp = smtplib.SMTP(mailServer, mailPort, myHostname)
             if mailUsername != '':
                 smtp.login(mailUsername, mailPassword)
-            smtp.sendmail(rawFrom, rawTo, mailString.encode('utf-8'))
+
+            # We're using the parsing above to do basic validation, but instead of
+            # failing it may munge the address it returns. So we should *not* use
+            # that parsed address, as it may not match any validation done
+            # elsewhere.
+            smtp.sendmail(mailFrom, mailTo, mailString.encode('utf-8'))
             smtp.quit()
         except Exception as origException:
             twisted.python.log.err()
path: sydent/util/emailutils.py