aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Mailman/CSRFcheck.py73
-rw-r--r--Mailman/Cgi/admin.py32
-rw-r--r--Mailman/Defaults.py.in5
-rw-r--r--Mailman/htmlformat.py13
-rw-r--r--NEWS8
5 files changed, 122 insertions, 9 deletions
diff --git a/Mailman/CSRFcheck.py b/Mailman/CSRFcheck.py
new file mode 100644
index 00000000..a3b6885a
--- /dev/null
+++ b/Mailman/CSRFcheck.py
@@ -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
diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py
index 569aa61c..d881241c 100644
--- a/Mailman/Cgi/admin.py
+++ b/Mailman/Cgi/admin.py
@@ -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.htmlformat import *
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 @@ except NameError:
True = 1
False = 0
+AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin)
+
def main():
@@ -83,6 +86,18 @@ def main():
# 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 @@ def main():
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 @@ def option_help(mlist, varhelp):
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 @@ def show_results(mlist, doc, category, subcat, cgidata):
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"))),
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index 14321e99..9aebaea2 100644
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -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 @@ ALLOW_SITE_ADMIN_COOKIES = No
# 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.
diff --git a/Mailman/htmlformat.py b/Mailman/htmlformat.py
index 7152e1f0..5d70ad28 100644
--- a/Mailman/htmlformat.py
+++ b/Mailman/htmlformat.py
@@ -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 mm_cfg
from Mailman import Utils
from Mailman.i18n import _
+from Mailman.CSRFcheck import csrf_token
+
SPACE = ' '
EMPTYSTRING = ''
NL = '\n'
@@ -402,11 +404,14 @@ class Center(StdContainer):
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 @@ class Form(Container):
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
diff --git a/NEWS b/NEWS
index 764a4c2d..96d6ff58 100644
--- a/NEWS
+++ b/NEWS
@@ -12,6 +12,14 @@ Here is a history of user visible changes to Mailman.
- 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