diff options
| author | Barry Warsaw | 2008-02-27 01:26:18 -0500 |
|---|---|---|
| committer | Barry Warsaw | 2008-02-27 01:26:18 -0500 |
| commit | a1c73f6c305c7f74987d99855ba59d8fa823c253 (patch) | |
| tree | 65696889450862357c9e05c8e9a589f1bdc074ac /Mailman/SecurityManager.py | |
| parent | 3f31f8cce369529d177cfb5a7c66346ec1e12130 (diff) | |
| download | mailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.tar.gz mailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.tar.zst mailman-a1c73f6c305c7f74987d99855ba59d8fa823c253.zip | |
Diffstat (limited to 'Mailman/SecurityManager.py')
| -rw-r--r-- | Mailman/SecurityManager.py | 306 |
1 files changed, 0 insertions, 306 deletions
diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py deleted file mode 100644 index 1132641bc..000000000 --- a/Mailman/SecurityManager.py +++ /dev/null @@ -1,306 +0,0 @@ -# Copyright (C) 1998-2008 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. - -"""Handle passwords and sanitize approved messages.""" - -# There are current 5 roles defined in Mailman, as codified in Defaults.py: -# user, list-creator, list-moderator, list-admin, site-admin. -# -# Here's how we do cookie based authentication. -# -# Each role (see above) has an associated password, which is currently the -# only way to authenticate a role (in the future, we'll authenticate a -# user and assign users to roles). -# -# Each cookie has the following ingredients: the authorization context's -# secret (i.e. the password, and a timestamp. We generate an SHA1 hex -# digest of these ingredients, which we call the 'mac'. We then marshal -# up a tuple of the timestamp and the mac, hexlify that and return that as -# a cookie keyed off the authcontext. Note that authenticating the user -# also requires the user's email address to be included in the cookie. -# -# The verification process is done in CheckCookie() below. It extracts -# the cookie, unhexlifies and unmarshals the tuple, extracting the -# timestamp. Using this, and the shared secret, the mac is calculated, -# and it must match the mac passed in the cookie. If so, they're golden, -# otherwise, access is denied. -# -# It is still possible for an adversary to attempt to brute force crack -# the password if they obtain the cookie, since they can extract the -# timestamp and create macs based on password guesses. They never get a -# cleartext version of the password though, so security rests on the -# difficulty and expense of retrying the cgi dialog for each attempt. It -# also relies on the security of SHA1. - -import os -import re -import sha -import time -import urllib -import Cookie -import logging -import marshal -import binascii - -from urlparse import urlparse - -from Mailman import Defaults -from Mailman import Errors -from Mailman import Utils -from Mailman import passwords -from Mailman.configuration import config - -log = logging.getLogger('mailman.error') -dlog = logging.getLogger('mailman.debug') - -SLASH = '/' - - - -class SecurityManager: - def AuthContextInfo(self, authcontext, user=None): - # authcontext may be one of AuthUser, AuthListModerator, - # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator - # context. - # - # user is ignored unless authcontext is AuthUser - # - # Return the authcontext's secret and cookie key. If the authcontext - # doesn't exist, return the tuple (None, None). If authcontext is - # AuthUser, but the user isn't a member of this mailing list, a - # NotAMemberError will be raised. If the user's secret is None, raise - # a MMBadUserError. - key = urllib.quote(self.fqdn_listname) + '+' - if authcontext == Defaults.AuthUser: - if user is None: - # A bad system error - raise TypeError('No user supplied for AuthUser context') - secret = self.getMemberPassword(user) - userdata = urllib.quote(Utils.ObscureEmail(user), safe='') - key += 'user+%s' % userdata - elif authcontext == Defaults.AuthListModerator: - secret = self.mod_password - key += 'moderator' - elif authcontext == Defaults.AuthListAdmin: - secret = self.password - key += 'admin' - # BAW: AuthCreator - elif authcontext == Defaults.AuthSiteAdmin: - sitepass = Utils.get_global_password() - if config.ALLOW_SITE_ADMIN_COOKIES and sitepass: - secret = sitepass - key = 'site' - else: - # BAW: this should probably hand out a site password based - # cookie, but that makes me a bit nervous, so just treat site - # admin as a list admin since there is currently no site - # admin-only functionality. - secret = self.password - key += 'admin' - else: - return None, None - return key, secret - - def Authenticate(self, authcontexts, response, user=None): - # Given a list of authentication contexts, check to see if the - # response matches one of the passwords. authcontexts must be a - # sequence, and if it contains the context AuthUser, then the user - # argument must not be None. - # - # Return the authcontext from the argument sequence that matches the - # response, or UnAuthorized. - for ac in authcontexts: - if ac == Defaults.AuthCreator: - ok = Utils.check_global_password(response, siteadmin=False) - if ok: - return Defaults.AuthCreator - elif ac == Defaults.AuthSiteAdmin: - ok = Utils.check_global_password(response) - if ok: - return Defaults.AuthSiteAdmin - elif ac == Defaults.AuthListAdmin: - # The password for the list admin and list moderator are not - # kept as plain text, but instead as an sha hexdigest. The - # response being passed in is plain text, so we need to - # digestify it first. - key, secret = self.AuthContextInfo(ac) - if secret is None: - continue - if passwords.check_response(secret, response): - return ac - elif ac == Defaults.AuthListModerator: - # The list moderator password must be sha'd - key, secret = self.AuthContextInfo(ac) - if secret and passwords.check_response(secret, response): - return ac - elif ac == Defaults.AuthUser: - if user is not None: - try: - if self.authenticateMember(user, response): - return ac - except Errors.NotAMemberError: - pass - else: - # What is this context??? - log.error('Bad authcontext: %s', ac) - raise ValueError('Bad authcontext: %s' % ac) - return Defaults.UnAuthorized - - def WebAuthenticate(self, authcontexts, response, user=None): - # Given a list of authentication contexts, check to see if the cookie - # contains a matching authorization, falling back to checking whether - # the response matches one of the passwords. authcontexts must be a - # sequence, and if it contains the context AuthUser, then the user - # argument should not be None. - # - # Returns a flag indicating whether authentication succeeded or not. - for ac in authcontexts: - ok = self.CheckCookie(ac, user) - if ok: - return True - # Check passwords - ac = self.Authenticate(authcontexts, response, user) - if ac: - print self.MakeCookie(ac, user) - return True - return False - - def _cookie_path(self): - script_name = os.environ.get('SCRIPT_NAME', '') - return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH - - def MakeCookie(self, authcontext, user=None): - key, secret = self.AuthContextInfo(authcontext, user) - if key is None or secret is None or not isinstance(secret, basestring): - raise ValueError - # Timestamp - issued = int(time.time()) - # Get a digest of the secret, plus other information. - mac = sha.new(secret + repr(issued)).hexdigest() - # Create the cookie object. - c = Cookie.SimpleCookie() - c[key] = binascii.hexlify(marshal.dumps((issued, mac))) - c[key]['path'] = self._cookie_path() - # We use session cookies, so don't set 'expires' or 'max-age' keys. - # Set the RFC 2109 required header. - c[key]['version'] = 1 - return c - - def ZapCookie(self, authcontext, user=None): - # We can throw away the secret. - key, secret = self.AuthContextInfo(authcontext, user) - # Logout of the session by zapping the cookie. For safety both set - # max-age=0 (as per RFC2109) and set the cookie data to the empty - # string. - c = Cookie.SimpleCookie() - c[key] = '' - c[key]['path'] = self._cookie_path() - c[key]['max-age'] = 0 - # Don't set expires=0 here otherwise it'll force a persistent cookie - c[key]['version'] = 1 - return c - - def CheckCookie(self, authcontext, user=None): - # Two results can occur: we return 1 meaning the cookie authentication - # succeeded for the authorization context, we return 0 meaning the - # authentication failed. - # - # Dig out the cookie data, which better be passed on this cgi - # environment variable. If there's no cookie data, we reject the - # authentication. - cookiedata = os.environ.get('HTTP_COOKIE') - if not cookiedata: - return False - # We can't use the Cookie module here because it isn't liberal in what - # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and - # you get a CookieError. :(. All we care about is accessing the - # cookie data via getitem, so we'll use our own parser, which returns - # a dictionary. - c = parsecookie(cookiedata) - # If the user was not supplied, but the authcontext is AuthUser, we - # can try to glean the user address from the cookie key. There may be - # more than one matching key (if the user has multiple accounts - # subscribed to this list), but any are okay. - if authcontext == Defaults.AuthUser: - if user: - usernames = [user] - else: - usernames = [] - prefix = urllib.quote(self.fqdn_listname) + '+user+' - for k in c.keys(): - if k.startswith(prefix): - usernames.append(k[len(prefix):]) - # If any check out, we're golden. Note: '@'s are no longer legal - # values in cookie keys. - for user in [Utils.UnobscureEmail(u) for u in usernames]: - ok = self.__checkone(c, authcontext, user) - if ok: - return True - return False - else: - return self.__checkone(c, authcontext, user) - - def __checkone(self, c, authcontext, user): - # Do the guts of the cookie check, for one authcontext/user - # combination. - try: - key, secret = self.AuthContextInfo(authcontext, user) - except Errors.NotAMemberError: - return False - if key not in c or not isinstance(secret, basestring): - return False - # Undo the encoding we performed in MakeCookie() above. BAW: I - # believe this is safe from exploit because marshal can't be forced to - # load recursive data structures, and it can't be forced to execute - # any unexpected code. The worst that can happen is that either the - # client will have provided us bogus data, in which case we'll get one - # of the caught exceptions, or marshal format will have changed, in - # which case, the cookie decoding will fail. In either case, we'll - # simply request reauthorization, resulting in a new cookie being - # returned to the client. - try: - data = marshal.loads(binascii.unhexlify(c[key])) - issued, received_mac = data - except (EOFError, ValueError, TypeError, KeyError): - return False - # Make sure the issued timestamp makes sense - now = time.time() - if now < issued: - return False - # Calculate what the mac ought to be based on the cookie's timestamp - # and the shared secret. - mac = sha.new(secret + repr(issued)).hexdigest() - if mac <> received_mac: - return False - # Authenticated! - return True - - - -splitter = re.compile(';\s*') - -def parsecookie(s): - c = {} - for line in s.splitlines(): - for p in splitter.split(line): - try: - k, v = p.split('=', 1) - except ValueError: - pass - else: - c[k] = v - return c |
