------------------------------------------------------------
revno: 1337
committer: Mark Sapiro <msap...@value.net>
branch nick: 2.1
timestamp: Sun 2012-02-05 13:19:39 -0800
message:
  Added Tokio Kikuchi's Cross-site Request Forgery hardening to the admin UI.
added:
  Mailman/CSRFcheck.py
modified:
  Mailman/Cgi/admin.py
  Mailman/Defaults.py.in
  Mailman/htmlformat.py
  NEWS


--
lp:mailman/2.1
https://code.launchpad.net/~mailman-coders/mailman/2.1

Your team Mailman Checkins is subscribed to branch lp:mailman/2.1.
To unsubscribe from this branch go to 
https://code.launchpad.net/~mailman-coders/mailman/2.1/+edit-subscription
=== added file 'Mailman/CSRFcheck.py'
--- Mailman/CSRFcheck.py	1970-01-01 00:00:00 +0000
+++ Mailman/CSRFcheck.py	2012-02-05 21:19:39 +0000
@@ -0,0 +1,73 @@
+# Copyright (C) 2011-2012 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+""" Cross-Site Request Forgery checker """
+
+import time
+import marshal
+import binascii
+
+from Mailman import mm_cfg
+from Mailman.Utils import sha_new
+
+keydict = {
+    'user':      mm_cfg.AuthUser,
+    'poster':    mm_cfg.AuthListPoster,
+    'moderator': mm_cfg.AuthListModerator,
+    'admin':     mm_cfg.AuthListAdmin,
+    'site':      mm_cfg.AuthSiteAdmin,
+}
+
+
+
+def csrf_token(mlist, contexts, user=None):
+    """ create token by mailman cookie generation algorithm """
+
+    for context in contexts:
+        key, secret = mlist.AuthContextInfo(context, user)
+        if key:
+            break
+    else:
+        return None     # not authenticated
+    issued = int(time.time())
+    mac = sha_new(secret + `issued`).hexdigest()
+    keymac = '%s:%s' % (key, mac)
+    token = binascii.hexlify(marshal.dumps((issued, keymac)))
+    return token
+
+def csrf_check(mlist, token):
+    """ check token by mailman cookie validation algorithm """
+
+    try:
+        issued, keymac = marshal.loads(binascii.unhexlify(token))
+        key, received_mac = keymac.split(':', 1)
+        klist, key = key.split('+', 1)
+        assert klist == mlist.internal_name()
+        if '+' in key:
+            key, user = key.split('+', 1)
+        else:
+            user = None
+        context = keydict.get(key)
+        key, secret = mlist.AuthContextInfo(context, user)
+        assert key
+        mac = sha_new(secret + `issued`).hexdigest()
+        if (mac == received_mac 
+            and 0 < time.time() - issued < mm_cfg.FORM_LIFETIME):
+            return True
+        return False
+    except (AssertionError, ValueError, TypeError):
+        return False

=== modified file 'Mailman/Cgi/admin.py'
--- Mailman/Cgi/admin.py	2011-04-25 23:52:35 +0000
+++ Mailman/Cgi/admin.py	2012-02-05 21:19:39 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2012 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -41,6 +41,7 @@
 from Mailman.Cgi import Auth
 from Mailman.Logging.Syslog import syslog
 from Mailman.Utils import sha_new
+from Mailman.CSRFcheck import csrf_check
 
 # Set up i18n
 _ = i18n._
@@ -55,6 +56,8 @@
     True = 1
     False = 0
 
+AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin)
+
 
 
 def main():
@@ -83,6 +86,18 @@
     # If the user is not authenticated, we're done.
     cgidata = cgi.FieldStorage(keep_blank_values=1)
 
+    # CSRF check
+    safe_params = ['VARHELP', 'adminpw', 'admlogin']
+    params = cgidata.keys()
+    if set(params) - set(safe_params):
+        csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token'))
+    else:
+        csrf_checked = True
+    # if password is present, void cookie to force password authentication.
+    if cgidata.getvalue('adminpw'):
+        os.environ['HTTP_COOKIE'] = ''
+        csrf_checked = True
+
     if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin,
                                   mm_cfg.AuthSiteAdmin),
                                  cgidata.getvalue('adminpw', '')):
@@ -174,8 +189,12 @@
         signal.signal(signal.SIGTERM, sigterm_handler)
 
         if cgidata.keys():
-            # There are options to change
-            change_options(mlist, category, subcat, cgidata, doc)
+            if csrf_checked:
+                # There are options to change
+                change_options(mlist, category, subcat, cgidata, doc)
+            else:
+                doc.addError(
+                  _('The form lifetime has expired. (request forgery check)'))
             # Let the list sanity check the changed values
             mlist.CheckValues()
         # Additional sanity checks
@@ -362,7 +381,7 @@
         url = '%s/%s/%s' % (mlist.GetScriptURL('admin'), category, subcat)
     else:
         url = '%s/%s' % (mlist.GetScriptURL('admin'), category)
-    form = Form(url)
+    form = Form(url, mlist=mlist, contexts=AUTH_CONTEXTS)
     valtab = Table(cellspacing=3, cellpadding=4, width='100%')
     add_options_table_item(mlist, category, subcat, valtab, item, detailsp=0)
     form.AddItem(valtab)
@@ -408,9 +427,10 @@
         encoding = 'multipart/form-data'
     if subcat:
         form = Form('%s/%s/%s' % (adminurl, category, subcat),
-                    encoding=encoding)
+                    encoding=encoding, mlist=mlist, contexts=AUTH_CONTEXTS)
     else:
-        form = Form('%s/%s' % (adminurl, category), encoding=encoding)
+        form = Form('%s/%s' % (adminurl, category), 
+                    encoding=encoding, mlist=mlist, contexts=AUTH_CONTEXTS)
     # This holds the two columns of links
     linktable = Table(valign='top', width='100%')
     linktable.AddRow([Center(Bold(_("Configuration Categories"))),

=== modified file 'Mailman/Defaults.py.in'
--- Mailman/Defaults.py.in	2011-10-04 21:53:13 +0000
+++ Mailman/Defaults.py.in	2012-02-05 21:19:39 +0000
@@ -1,6 +1,6 @@
 # -*- python -*-
 
-# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2012 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -108,6 +108,9 @@
 # expire that many seconds following their last use.
 AUTHENTICATION_COOKIE_LIFETIME = 0
 
+# Form lifetime is set against Cross Site Request Forgery.
+FORM_LIFETIME = hours(1)
+
 # Command that is used to convert text/html parts into plain text.  This
 # should output results to standard output.  %(filename)s will contain the
 # name of the temporary file that the program should operate on.

=== modified file 'Mailman/htmlformat.py'
--- Mailman/htmlformat.py	2007-11-25 08:04:30 +0000
+++ Mailman/htmlformat.py	2012-02-05 21:19:39 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
+# Copyright (C) 1998-2012 by the Free Software Foundation, Inc.
 #
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
@@ -34,6 +34,8 @@
 from Mailman import Utils
 from Mailman.i18n import _
 
+from Mailman.CSRFcheck import csrf_token
+
 SPACE = ' '
 EMPTYSTRING = ''
 NL = '\n'
@@ -402,11 +404,14 @@
     tag = 'center'
 
 class Form(Container):
-    def __init__(self, action='', method='POST', encoding=None, *items):
+    def __init__(self, action='', method='POST', encoding=None, 
+                       mlist=None, contexts=None, *items):
         apply(Container.__init__, (self,) +  items)
         self.action = action
         self.method = method
         self.encoding = encoding
+        self.mlist = mlist
+        self.contexts = contexts
 
     def set_action(self, action):
         self.action = action
@@ -418,6 +423,10 @@
             encoding = 'enctype="%s"' % self.encoding
         output = '\n%s<FORM action="%s" method="%s" %s>\n' % (
             spaces, self.action, self.method, encoding)
+        if self.mlist:
+            output = output + \
+                '<input type="hidden" name="csrf_token" value="%s">\n' \
+                % csrf_token(self.mlist, self.contexts)
         output = output + Container.Format(self, indent+2)
         output = '%s\n%s</FORM>\n' % (output, spaces)
         return output

=== modified file 'NEWS'
--- NEWS	2011-12-31 22:22:39 +0000
+++ NEWS	2012-02-05 21:19:39 +0000
@@ -12,6 +12,14 @@
 
     - An XSS vulnerability, CVE-2011-0707, has been fixed.
 
+    - The web admin interface has been hardened against CSRF attacks by adding
+      a hidden, encrypted token with a time stamp to form submissions and not
+      accepting authentication by cookie if the token is missing, invalid or
+      older than the new mm_cfg.py setting FORM_LIFETIME which defaults to one
+      hour.  Posthumous thanks go to Tokio Kikuchi for this implementation
+      which is only one of his many contributions to Mailman prior to his
+      death from cancer on 14 January 2012.
+
   New Features
 
     - Eliminated the list cache from the qrunners.  Indirect self-references

_______________________________________________
Mailman-checkins mailing list
Mailman-checkins@python.org
Unsubscribe: 
http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org

Reply via email to