diff options
author | Mark Sapiro <msapiro@value.net> | 2012-02-05 13:19:39 -0800 |
---|---|---|
committer | Mark Sapiro <msapiro@value.net> | 2012-02-05 13:19:39 -0800 |
commit | fdd6141b978cdc0876263d962f996eb88964537b (patch) | |
tree | 6836790556e26d896b791946fc60df5d0f88ab8a /Mailman | |
parent | 3c1fe7bcb3c10650cd039c800aa1356886586873 (diff) | |
download | mailman2-fdd6141b978cdc0876263d962f996eb88964537b.tar.gz mailman2-fdd6141b978cdc0876263d962f996eb88964537b.tar.xz mailman2-fdd6141b978cdc0876263d962f996eb88964537b.zip |
Added Tokio Kikuchi's Cross-site Request Forgery hardening to the admin UI.
Diffstat (limited to 'Mailman')
-rw-r--r-- | Mailman/CSRFcheck.py | 73 | ||||
-rw-r--r-- | Mailman/Cgi/admin.py | 32 | ||||
-rw-r--r-- | Mailman/Defaults.py.in | 5 | ||||
-rw-r--r-- | Mailman/htmlformat.py | 13 |
4 files changed, 114 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 |