diff --git a/SOURCES/mailman-CVE-2016-6893.patch b/SOURCES/mailman-CVE-2016-6893.patch new file mode 100644 index 0000000..bee2601 --- /dev/null +++ b/SOURCES/mailman-CVE-2016-6893.patch @@ -0,0 +1,250 @@ +diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py +index d187332..defc58d 100644 +--- a/Mailman/Cgi/admindb.py ++++ b/Mailman/Cgi/admindb.py +@@ -39,6 +39,7 @@ from Mailman.ListAdmin import readMessage + from Mailman.Cgi import Auth + from Mailman.htmlformat import * + from Mailman.Logging.Syslog import syslog ++from Mailman.CSRFcheck import csrf_check + + EMPTYSTRING = '' + NL = '\n' +@@ -51,6 +52,9 @@ i18n.set_language(mm_cfg.DEFAULT_SERVER_LANGUAGE) + EXCERPT_HEIGHT = 10 + EXCERPT_WIDTH = 76 + ++AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin, ++ mm_cfg.AuthListModerator) ++ + + + def helds_by_sender(mlist): +@@ -100,6 +104,18 @@ def main(): + # Make sure the user is authorized to see this page. + cgidata = cgi.FieldStorage(keep_blank_values=1) + ++ # CSRF check ++ safe_params = ['adminpw', 'admlogin', 'msgid', 'sender', 'details'] ++ 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.AuthListModerator, + mm_cfg.AuthSiteAdmin), +@@ -177,7 +193,11 @@ def main(): + elif not details: + # This is a form submission + doc.SetTitle(_('%(realname)s Administrative Database Results')) +- process_form(mlist, doc, cgidata) ++ if csrf_checked: ++ process_form(mlist, doc, cgidata) ++ else: ++ doc.addError( ++ _('The form lifetime has expired. (request forgery check)')) + # Now print the results and we're done. Short circuit for when there + # are no pending requests, but be sure to save the results! + admindburl = mlist.GetScriptURL('admindb', absolute=1) +@@ -198,7 +218,7 @@ def main(): + mlist.Save() + return + +- form = Form(admindburl) ++ form = Form(admindburl, mlist=mlist, contexts=AUTH_CONTEXTS) + # Add the instructions template + if details == 'instructions': + doc.AddItem(Header( +diff --git a/Mailman/Cgi/edithtml.py b/Mailman/Cgi/edithtml.py +index ee1ccd0..5c9416e 100644 +--- a/Mailman/Cgi/edithtml.py ++++ b/Mailman/Cgi/edithtml.py +@@ -30,9 +30,12 @@ from Mailman import Errors + from Mailman.Cgi import Auth + from Mailman.Logging.Syslog import syslog + from Mailman import i18n ++from Mailman.CSRFcheck import csrf_check + + _ = i18n._ + ++AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin) ++ + + + def main(): +@@ -82,6 +85,18 @@ def main(): + # Must be authenticated to get any farther + cgidata = cgi.FieldStorage() + ++ # 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 ++ + # Editing the html for a list is limited to the list admin and site admin. + if not mlist.WebAuthenticate((mm_cfg.AuthListAdmin, + mm_cfg.AuthSiteAdmin), +@@ -126,7 +141,11 @@ def main(): + + try: + if cgidata.keys(): +- ChangeHTML(mlist, cgidata, template_name, doc) ++ if csrf_checked: ++ ChangeHTML(mlist, cgidata, template_name, doc) ++ else: ++ doc.addError( ++ _('The form lifetime has expired. (request forgery check)')) + FormatHTML(mlist, doc, template_name, template_info) + finally: + doc.AddItem(mlist.GetMailmanFooter()) +@@ -145,7 +164,8 @@ def FormatHTML(mlist, doc, template_name, template_info): + doc.AddItem(FontSize("+1", link)) + doc.AddItem('

') + doc.AddItem('


') +- form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name) ++ form = Form(mlist.GetScriptURL('edithtml') + '/' + template_name, ++ mlist=mlist, contexts=AUTH_CONTEXTS) + text = Utils.maketext(template_name, raw=1, mlist=mlist) + # MAS: Don't websafe twice. TextArea does it. + form.AddItem(TextArea('html_code', text, rows=40, cols=75)) +diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py +index ae701a7..328d9d5 100644 +--- a/Mailman/Cgi/options.py ++++ b/Mailman/Cgi/options.py +@@ -33,6 +33,7 @@ from Mailman import MemberAdaptor + from Mailman import i18n + from Mailman.htmlformat import * + from Mailman.Logging.Syslog import syslog ++from Mailman.CSRFcheck import csrf_check + + SLASH = '/' + SETLANGUAGE = -1 +@@ -47,6 +48,8 @@ except NameError: + True = 1 + False = 0 + ++AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin, ++ mm_cfg.AuthListModerator, mm_cfg.AuthUser) + + + def main(): +@@ -88,6 +91,19 @@ def main(): + # The total contents of the user's response + cgidata = cgi.FieldStorage(keep_blank_values=1) + ++ # CSRF check ++ safe_params = ['displang-button', 'language', 'email', 'password', 'login', ++ 'login-unsub', 'login-remind', 'VARHELP', 'UserOptions'] ++ 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('password'): ++ os.environ['HTTP_COOKIE'] = '' ++ csrf_checked = True ++ + # Set the language for the page. If we're coming from the listinfo cgi, + # we might have a 'language' key in the cgi data. That was an explicit + # preference to view the page in, so we should honor that here. If that's +@@ -269,6 +285,15 @@ def main(): + # options. The first set of checks does not require the list to be + # locked. + ++ # Before going further, get the result of CSRF check and do nothing ++ # if it has failed. ++ if csrf_checked == False: ++ doc.addError( ++ _('The form lifetime has expired. (request forgery check)')) ++ options_page(mlist, doc, user, cpuser, userlang) ++ print doc.Format() ++ return ++ + if cgidata.has_key('logout'): + print mlist.ZapCookie(mm_cfg.AuthUser, user) + loginpage(mlist, doc, user, language) +@@ -779,7 +804,8 @@ def options_page(mlist, doc, user, cpuser, userlang, message=''): + mlist.FormatButton('othersubs', + _('List my other subscriptions'))) + replacements[''] = ( +- mlist.FormatFormStart('options', user)) ++ mlist.FormatFormStart('options', user, mlist=mlist, ++ contexts=AUTH_CONTEXTS, user=user)) + replacements[''] = user + replacements[''] = presentable_user + replacements[''] = mlist.FormatButton( +diff --git a/Mailman/HTMLFormatter.py b/Mailman/HTMLFormatter.py +index dad51e7..3afe4bb 100644 +--- a/Mailman/HTMLFormatter.py ++++ b/Mailman/HTMLFormatter.py +@@ -28,6 +28,8 @@ from Mailman.htmlformat import * + + from Mailman.i18n import _ + ++from Mailman.CSRFcheck import csrf_token ++ + + EMPTYSTRING = '' + BR = '
' +@@ -314,12 +316,17 @@ class HTMLFormatter: + container.AddItem("") + return container + +- def FormatFormStart(self, name, extra=''): ++ def FormatFormStart(self, name, extra='', ++ mlist=None, contexts=None, user=None): + base_url = self.GetScriptURL(name) + if extra: + full_url = "%s/%s" % (base_url, extra) + else: + full_url = base_url ++ if mlist: ++ return ("""
++""" ++ % (full_url, csrf_token(mlist, contexts, user))) + return ('' % full_url) + + def FormatArchiveAnchor(self): +diff --git a/Mailman/htmlformat.py b/Mailman/htmlformat.py +index 2387096..e2ad2e4 100644 +--- a/Mailman/htmlformat.py ++++ b/Mailman/htmlformat.py +@@ -405,13 +405,14 @@ class Center(StdContainer): + + class Form(Container): + def __init__(self, action='', method='POST', encoding=None, +- mlist=None, contexts=None, *items): ++ mlist=None, contexts=None, user=None, *items): + apply(Container.__init__, (self,) + items) + self.action = action + self.method = method + self.encoding = encoding + self.mlist = mlist + self.contexts = contexts ++ self.user = user + + def set_action(self, action): + self.action = action +@@ -426,7 +427,7 @@ class Form(Container): + if self.mlist: + output = output + \ + '\n' \ +- % csrf_token(self.mlist, self.contexts) ++ % csrf_token(self.mlist, self.contexts, self.user) + output = output + Container.Format(self, indent+2) + output = '%s\n%s
\n' % (output, spaces) + return output diff --git a/SOURCES/mailman-CVE-2021-42097.patch b/SOURCES/mailman-CVE-2021-42097.patch new file mode 100644 index 0000000..cb2351d --- /dev/null +++ b/SOURCES/mailman-CVE-2021-42097.patch @@ -0,0 +1,128 @@ +diff --git a/Mailman/CSRFcheck.py b/Mailman/CSRFcheck.py +index a3b6885..73b003a 100644 +--- a/Mailman/CSRFcheck.py ++++ b/Mailman/CSRFcheck.py +@@ -18,11 +18,13 @@ + """ Cross-Site Request Forgery checker """ + + import time ++import urllib + import marshal + import binascii + + from Mailman import mm_cfg +-from Mailman.Utils import sha_new ++from Mailman.Logging.Syslog import syslog ++from Mailman.Utils import UnobscureEmail, sha_new + + keydict = { + 'user': mm_cfg.AuthUser, +@@ -37,6 +39,10 @@ keydict = { + def csrf_token(mlist, contexts, user=None): + """ create token by mailman cookie generation algorithm """ + ++ if user: ++ # Unmunge a munged email address. ++ user = UnobscureEmail(urllib.unquote(user)) ++ + for context in contexts: + key, secret = mlist.AuthContextInfo(context, user) + if key: +@@ -49,9 +55,8 @@ def csrf_token(mlist, contexts, user=None): + token = binascii.hexlify(marshal.dumps((issued, keymac))) + return token + +-def csrf_check(mlist, token): ++def csrf_check(mlist, token, options_user=None): + """ check token by mailman cookie validation algorithm """ +- + try: + issued, keymac = marshal.loads(binascii.unhexlify(token)) + key, received_mac = keymac.split(':', 1) +@@ -61,6 +66,17 @@ def csrf_check(mlist, token): + key, user = key.split('+', 1) + else: + user = None ++ if user: ++ # This is for CVE-2021-42097. The token is a user token because ++ # of the fix for CVE-2021-42096 but it must match the user for ++ # whom the options page is requested. ++ raw_user = UnobscureEmail(urllib.unquote(user)) ++ if options_user and options_user != raw_user: ++ syslog('mischief', ++ 'Form for user %s submitted with CSRF token ' ++ 'issued for %s.', ++ options_user, raw_user) ++ return False + context = keydict.get(key) + key, secret = mlist.AuthContextInfo(context, user) + assert key +diff --git a/Mailman/Cgi/options.py b/Mailman/Cgi/options.py +index 328d9d5..51c06b6 100644 +--- a/Mailman/Cgi/options.py ++++ b/Mailman/Cgi/options.py +@@ -48,9 +48,6 @@ except NameError: + True = 1 + False = 0 + +-AUTH_CONTEXTS = (mm_cfg.AuthListAdmin, mm_cfg.AuthSiteAdmin, +- mm_cfg.AuthListModerator, mm_cfg.AuthUser) +- + + def main(): + doc = Document() +@@ -95,14 +92,6 @@ def main(): + safe_params = ['displang-button', 'language', 'email', 'password', 'login', + 'login-unsub', 'login-remind', 'VARHELP', 'UserOptions'] + 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('password'): +- os.environ['HTTP_COOKIE'] = '' +- csrf_checked = True + + # Set the language for the page. If we're coming from the listinfo cgi, + # we might have a 'language' key in the cgi data. That was an explicit +@@ -131,6 +120,16 @@ def main(): + user = Utils.LCDomain(Utils.UnobscureEmail(SLASH.join(parts[1:]))) + + # Avoid cross-site scripting attacks ++ if set(params) - set(safe_params): ++ csrf_checked = csrf_check(mlist, cgidata.getfirst('csrf_token'), ++ Utils.UnobscureEmail(urllib.unquote(user))) ++ else: ++ csrf_checked = True ++ # if password is present, void cookie to force password authentication. ++ if cgidata.getfirst('password'): ++ os.environ['HTTP_COOKIE'] = '' ++ csrf_checked = True ++ + safeuser = Utils.websafe(user) + try: + Utils.ValidateEmail(user) +@@ -804,8 +803,9 @@ def options_page(mlist, doc, user, cpuser, userlang, message=''): + mlist.FormatButton('othersubs', + _('List my other subscriptions'))) + replacements[''] = ( ++ # Always make the CSRF token for the user. CVE-2021-42096 + mlist.FormatFormStart('options', user, mlist=mlist, +- contexts=AUTH_CONTEXTS, user=user)) ++ contexts=[mm_cfg.AuthUser], user=user)) + replacements[''] = user + replacements[''] = presentable_user + replacements[''] = mlist.FormatButton( +diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py +index 4f6aa34..74b7a67 100644 +--- a/Mailman/SecurityManager.py ++++ b/Mailman/SecurityManager.py +@@ -104,6 +104,7 @@ class SecurityManager: + if user is None: + # A bad system error + raise TypeError, 'No user supplied for AuthUser context' ++ user = Utils.UnobscureEmail(urllib.unquote(user)) + secret = self.getMemberPassword(user) + userdata = urllib.quote(Utils.ObscureEmail(user), safe='') + key += 'user+%s' % userdata diff --git a/SOURCES/mailman-CVE-2021-44227.patch b/SOURCES/mailman-CVE-2021-44227.patch new file mode 100644 index 0000000..6afb82a --- /dev/null +++ b/SOURCES/mailman-CVE-2021-44227.patch @@ -0,0 +1,82 @@ +diff --git a/Mailman/CSRFcheck.py b/Mailman/CSRFcheck.py +index 73b003a..4328066 100644 +--- a/Mailman/CSRFcheck.py ++++ b/Mailman/CSRFcheck.py +@@ -55,7 +55,7 @@ def csrf_token(mlist, contexts, user=None): + token = binascii.hexlify(marshal.dumps((issued, keymac))) + return token + +-def csrf_check(mlist, token, options_user=None): ++def csrf_check(mlist, token, cgi_user=None): + """ check token by mailman cookie validation algorithm """ + try: + issued, keymac = marshal.loads(binascii.unhexlify(token)) +@@ -66,12 +66,25 @@ def csrf_check(mlist, token, options_user=None): + key, user = key.split('+', 1) + else: + user = None ++ # Don't allow unprivileged tokens for admin or admindb. ++ if cgi_user == 'admin': ++ if key not in ('admin', 'site'): ++ syslog('mischief', ++ 'admin form submitted with CSRF token issued for %s.', ++ key + '+' + user if user else key) ++ return False ++ elif cgi_user == 'admindb': ++ if key not in ('moderator', 'admin', 'site'): ++ syslog('mischief', ++ 'admindb form submitted with CSRF token issued for %s.', ++ key + '+' + user if user else key) ++ return False + if user: + # This is for CVE-2021-42097. The token is a user token because + # of the fix for CVE-2021-42096 but it must match the user for + # whom the options page is requested. + raw_user = UnobscureEmail(urllib.unquote(user)) +- if options_user and options_user != raw_user: ++ if cgi_user and cgi_user != raw_user: + syslog('mischief', + 'Form for user %s submitted with CSRF token ' + 'issued for %s.', +diff --git a/Mailman/Cgi/admin.py b/Mailman/Cgi/admin.py +index 6a391df..7df4726 100644 +--- a/Mailman/Cgi/admin.py ++++ b/Mailman/Cgi/admin.py +@@ -91,7 +91,8 @@ def main(): + 'letter', 'chunk', 'findmember'] + params = cgidata.keys() + if set(params) - set(safe_params): +- csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token')) ++ csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token'), ++ 'admin') + else: + csrf_checked = True + # if password is present, void cookie to force password authentication. +diff --git a/Mailman/Cgi/admindb.py b/Mailman/Cgi/admindb.py +index defc58d..ea8bf78 100644 +--- a/Mailman/Cgi/admindb.py ++++ b/Mailman/Cgi/admindb.py +@@ -108,7 +108,8 @@ def main(): + safe_params = ['adminpw', 'admlogin', 'msgid', 'sender', 'details'] + params = cgidata.keys() + if set(params) - set(safe_params): +- csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token')) ++ csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token'), ++ 'admindb') + else: + csrf_checked = True + # if password is present, void cookie to force password authentication. +diff --git a/Mailman/Cgi/edithtml.py b/Mailman/Cgi/edithtml.py +index 5c9416e..678e43c 100644 +--- a/Mailman/Cgi/edithtml.py ++++ b/Mailman/Cgi/edithtml.py +@@ -89,7 +89,8 @@ def main(): + safe_params = ['VARHELP', 'adminpw', 'admlogin'] + params = cgidata.keys() + if set(params) - set(safe_params): +- csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token')) ++ csrf_checked = csrf_check(mlist, cgidata.getvalue('csrf_token'), ++ 'admin') + else: + csrf_checked = True + # if password is present, void cookie to force password authentication. diff --git a/SPECS/mailman.spec b/SPECS/mailman.spec index 6f09ba5..5f306cc 100644 --- a/SPECS/mailman.spec +++ b/SPECS/mailman.spec @@ -1,10 +1,11 @@ +%global __python %{__python2} # Turn off the brp-python-bytecompile script %global __os_install_post %(echo '%{__os_install_post}' | sed -e 's!/usr/lib[^[:space:]]*/brp-python-bytecompile[[:space:]].*$!!g') Summary: Mailing list manager with built in Web access Name: mailman Version: 2.1.15 -Release: 30%{?dist} +Release: 30%{?dist}.2 Epoch: 3 Group: Applications/Internet Source0: ftp://ftp.gnu.org/pub/gnu/mailman/mailman-%{version}.tgz @@ -48,6 +49,9 @@ Patch27: mailman-2_1-xss_vulnerability.patch Patch28: mailman-findmember_decode.patch Patch29: mailman-long_text_description.patch Patch30: mailman-cve_2018_0618.patch +Patch31: mailman-CVE-2016-6893.patch +Patch32: mailman-CVE-2021-42097.patch +Patch33: mailman-CVE-2021-44227.patch License: GPLv2+ @@ -581,6 +585,15 @@ exit 0 %dir %attr(775,root,%{mmgroup}) %{lockdir} %changelog +* Sun Nov 28 2021 Martin Osvald - 3:2.1.15-30.2 +- Fix for CVE-2021-44227 +- Resolves: #2026866 + +* Tue Nov 09 2021 Martin Osvald - 3:2.1.15-30.1 +- Fix for CVE-2016-6893 +- Fix for CVE-2021-42097 +- Resolves: #2024884, #2020688 + * Wed Jul 31 2019 Pavel Zhukov - 3:2.1.15-30 - Resolves: #1599692 - Sanitize input on listinfo page (CVE-2018-0618)