[tor-commits] [bridgedb/master] Fix #12086; disallow emails not to our domain.

isis at torproject.org isis at torproject.org
Fri Jun 6 23:39:14 UTC 2014


commit 5e6258041d492c0748e03079f366bbb0d06d53f9
Author: Isis Lovecruft <isis at torproject.org>
Date:   Wed May 28 23:05:20 2014 +0000

    Fix #12086; disallow emails not to our domain.
    
    At the SMTP layer, we previously only checked that the email address
    within the SMTP 'RCPT TO:' command (after being stripped of any '+'
    aliases) as well as the email address in the 'To:' header both had a
    username equal to the configured EMAIL_USERNAME. It didn't check that
    these email addresses were the same, nor did it check the domain at all.
    
    This refactors the ``bridgedb.email.server.MailDelivery.validateTo()``
    method to check that the domain name in the SMTP 'RCPT TO:' command is
    either our domain or a subdomain of it.
    
    At the email layer, we now check that the email was sent to a domain
    which is either our domain or a subdomain of it. We also check that the
    username matches our configured username (after '+' aliases have been
    removed).
    
     * FIXES #12086, a bug where BridgeDB would accept emails addressed to
       addresses such as 'givemebridges at serious.ly'.
     * CHANGES MailDelivery.validateTo() to check domain names and usernames
       in the SMTP 'RCPT TO' command that an incoming email was received
       for.
     * RENAME MailMessage.getRecipient() → MailMessage.getMailFrom() in
       bridgedb.email.server module.
     * CHANGES MailMessage.getMailFrom() to check domain names and usernames
       in the 'To:' header of an incoming email.
---
 lib/bridgedb/email/server.py           |   71 ++++++++++++++++++++++++--------
 lib/bridgedb/test/test_email_server.py |   18 ++++----
 2 files changed, 62 insertions(+), 27 deletions(-)

diff --git a/lib/bridgedb/email/server.py b/lib/bridgedb/email/server.py
index 79b4a59..593713b 100644
--- a/lib/bridgedb/email/server.py
+++ b/lib/bridgedb/email/server.py
@@ -506,7 +506,7 @@ class MailMessage(object):
             else:
                 return client
 
-    def getRecipient(self, incoming):
+    def getMailFrom(self, incoming):
         """Find our address in the recipients list of the **incoming** message.
 
         :type incoming: :api:`twisted.mail.smtp.rfc822.Message`
@@ -516,21 +516,42 @@ class MailMessage(object):
         :return: Our address from the recipients list. If we can't find it
             return our default ``SMTP_FROM_ADDRESS`` from the config file.
         """
-        ourAddress = self.context.fromAddr
+        logging.debug("Searching for our email address in 'To:' header...")
+
+        ours = None
 
         try:
-            ourAddress = smtp.Address(ourAddress)
+            ourAddress = smtp.Address(self.context.fromAddr)
             allRecipients = incoming.getaddrlist("To")
+
             for _, addr in allRecipients:
                 recipient = smtp.Address(addr)
-                # See if the user looks familiar. We do a 'find' instead of
-                # compare because we might have a '+' address here.
-                if recipient.local.find(ourAddress.local) != -1:
-                    return '@'.join([recipient.local, recipient.domain])
-        except smtp.AddressError as error:
-            logging.warn(error)
+                if not (ourAddress.domain in recipient.domain):
+                    logging.debug(("Not our domain (%s) or subdomain, skipping"
+                                   " email address: %s")
+                                  % (ourAddress.domain, str(recipient)))
+                    continue
+                # The recipient's username should at least start with ours,
+                # but it still might be a '+' address.
+                if not recipient.local.startswith(ourAddress.local):
+                    logging.debug(("Username doesn't begin with ours, skipping"
+                                   " email address: %s") % str(recipient))
+                    continue
+                # Ignore everything after the first '+', if there is one.
+                beforePlus = recipient.local.split('+', 1)[0]
+                if beforePlus == ourAddress.local:
+                    ours = str(recipient)
+            if not ours:
+                raise BadEmail(allRecipients)
 
-        return ourAddress
+        except Exception as error:
+            logging.error(("Couldn't find our email address in incoming email "
+                           "headers: %r" % error))
+            # Just return the email address that we're configured to use:
+            ours = self.context.fromAddr
+
+        logging.debug("Found our email address: %s." % ours)
+        return ours
 
     def getCanonicalDomain(self, domain):
         try:
@@ -571,7 +592,7 @@ class MailMessage(object):
         d.addErrback(_replyEB)
 
         incoming = self.getIncomingMessage()
-        recipient = self.getRecipient(incoming)
+        recipient = self.getMailFrom(incoming)
         client = self.getClientAddress(incoming)
 
         if not client:
@@ -693,13 +714,27 @@ class MailDelivery(object):
         :returns: A parameterless function which returns an instance of
             :class:`SMTPMessage`.
         """
-        u = user.dest.local
-        # Hasplus? If yes, strip '+foo'
-        idx = u.find('+')
-        if idx != -1:
-            u = u[:idx]
-        if u != self.context.username:
-            raise smtp.SMTPBadRcpt(user)
+        logging.debug("Validating SMTP 'RCPT TO:' email address...")
+
+        recipient = user.dest
+        ourAddress = smtp.Address(self.context.smtpFromAddr)
+
+        if not (ourAddress.domain in recipient.domain):
+            logging.debug(("Not our domain (%s) or subdomain, skipping"
+                           " SMTP 'RCPT TO' address: %s")
+                          % (ourAddress.domain, str(recipient)))
+            raise smtp.SMTPBadRcpt(str(recipient))
+        # The recipient's username should at least start with ours,
+        # but it still might be a '+' address.
+        if not recipient.local.startswith(ourAddress.local):
+            logging.debug(("Username doesn't begin with ours, skipping"
+                           " SMTP 'RCPT TO' address: %s") % str(recipient))
+            raise smtp.SMTPBadRcpt(str(recipient))
+        # Ignore everything after the first '+', if there is one.
+        beforePlus = recipient.local.split('+', 1)[0]
+        if beforePlus != ourAddress.local:
+            raise smtp.SMTPBadRcpt(str(recipient))
+
         return lambda: MailMessage(self.context, self.fromCanonical)
 
 
diff --git a/lib/bridgedb/test/test_email_server.py b/lib/bridgedb/test/test_email_server.py
index 7015339..7aff08e 100644
--- a/lib/bridgedb/test/test_email_server.py
+++ b/lib/bridgedb/test/test_email_server.py
@@ -316,37 +316,37 @@ class MailMessageTests(unittest.TestCase):
         ]
         self.message.lines = lines
 
-    def test_MailMessage_getRecipient_notbridgedb_at_yikezors_dot_net(self):
-        """MailMessage.getRecipient() for an incoming email sent to any email
+    def test_MailMessage_getMailFrom_notbridgedb_at_yikezors_dot_net(self):
+        """MailMessage.getMailFrom() for an incoming email sent to any email
         address other than the one we're listening for should return our
         configured address, not the one in the incoming email.
         """
         self._getIncomingLines()
         self.message.lines[1] = 'To: notbridgedb at yikezors.net'
         incoming = self.message.getIncomingMessage()
-        recipient = self.message.getRecipient(incoming)
+        recipient = str(self.message.getMailFrom(incoming))
         self.assertEqual(recipient, self.context.fromAddr)
 
-    def test_MailMessage_getRecipient_givemebridges_at_seriously(self):
-        """MailMessage.getRecipient() for an incoming email sent to any email
+    def test_MailMessage_getMailFrom_givemebridges_at_seriously(self):
+        """MailMessage.getMailFrom() for an incoming email sent to any email
         address other than the one we're listening for should return our
         configured address, not the one in the incoming email.
         """
         self._getIncomingLines()
         self.message.lines[1] = 'To: givemebridges at serious.ly'
         incoming = self.message.getIncomingMessage()
-        recipient = self.message.getRecipient(incoming)
+        recipient = str(self.message.getMailFrom(incoming))
         self.assertEqual(recipient, self.context.fromAddr)
 
-    def test_MailMessage_getRecipient_bad_address(self):
-        """MailMessage.getRecipient() for an incoming email sent to a malformed
+    def test_MailMessage_getMailFrom_bad_address(self):
+        """MailMessage.getMailFrom() for an incoming email sent to a malformed
         email address should log an smtp.AddressError and then return our
         configured email address.
         """
         self._getIncomingLines()
         self.message.lines[1] = 'To: ><@><<<>>.foo'
         incoming = self.message.getIncomingMessage()
-        recipient = self.message.getRecipient(incoming)
+        recipient = str(self.message.getMailFrom(incoming))
         self.assertEqual(recipient, self.context.fromAddr)
 
     def test_MailMessage_reply_noFrom(self):





More information about the tor-commits mailing list