aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/SecurityManager.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Mailman/SecurityManager.py333
1 files changed, 333 insertions, 0 deletions
diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py
new file mode 100644
index 00000000..8b65738e
--- /dev/null
+++ b/Mailman/SecurityManager.py
@@ -0,0 +1,333 @@
+# Copyright (C) 1998,1999,2000,2001,2002 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, 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 time
+import sha
+import marshal
+import binascii
+import Cookie
+from types import StringType, TupleType
+from urlparse import urlparse
+
+try:
+ import crypt
+except ImportError:
+ crypt = None
+import md5
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import Errors
+from Mailman.Logging.Syslog import syslog
+
+
+
+class SecurityManager:
+ def InitVars(self):
+ # We used to set self.password here, from a crypted_password argument,
+ # but that's been removed when we generalized the mixin architecture.
+ # self.password is really a SecurityManager attribute, but it's set in
+ # MailList.InitVars().
+ self.mod_password = None
+ # Non configurable
+ self.passwords = {}
+
+ 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 = self.internal_name() + '+'
+ if authcontext == mm_cfg.AuthUser:
+ if user is None:
+ # A bad system error
+ raise TypeError, 'No user supplied for AuthUser context'
+ secret = self.getMemberPassword(user)
+ key += 'user+%s' % Utils.ObscureEmail(user)
+ elif authcontext == mm_cfg.AuthListModerator:
+ secret = self.mod_password
+ key += 'moderator'
+ elif authcontext == mm_cfg.AuthListAdmin:
+ secret = self.password
+ key += 'admin'
+ # BAW: AuthCreator
+ elif authcontext == mm_cfg.AuthSiteAdmin:
+ sitepass = Utils.get_global_password()
+ if mm_cfg.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 == mm_cfg.AuthCreator:
+ ok = Utils.check_global_password(response, siteadmin=0)
+ if ok:
+ return mm_cfg.AuthCreator
+ elif ac == mm_cfg.AuthSiteAdmin:
+ ok = Utils.check_global_password(response)
+ if ok:
+ return mm_cfg.AuthSiteAdmin
+ elif ac == mm_cfg.AuthListAdmin:
+ def cryptmatchp(response, secret):
+ try:
+ salt = secret[:2]
+ if crypt and crypt.crypt(response, salt) == secret:
+ return 1
+ return 0
+ except TypeError:
+ # BAW: Hard to say why we can get a TypeError here.
+ # SF bug report #585776 says crypt.crypt() can raise
+ # this if salt contains null bytes, although I don't
+ # know how that can happen (perhaps if a MM2.0 list
+ # with USE_CRYPT = 0 has been updated? Doubtful.
+ return 0
+ # 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. Note however, that for backwards
+ # compatibility reasons, we'll also check the admin response
+ # against the crypted and md5'd passwords, and if they match,
+ # we'll auto-migrate the passwords to sha.
+ key, secret = self.AuthContextInfo(ac)
+ if secret is None:
+ continue
+ sharesponse = sha.new(response).hexdigest()
+ upgrade = ok = 0
+ if sharesponse == secret:
+ ok = 1
+ elif md5.new(response).digest() == secret:
+ ok = 1
+ upgrade = 1
+ elif cryptmatchp(response, secret):
+ ok = 1
+ upgrade = 1
+ if upgrade:
+ save_and_unlock = 0
+ if not self.Locked():
+ self.Lock()
+ save_and_unlock = 1
+ try:
+ self.password = sharesponse
+ if save_and_unlock:
+ self.Save()
+ finally:
+ if save_and_unlock:
+ self.Unlock()
+ if ok:
+ return ac
+ elif ac == mm_cfg.AuthListModerator:
+ # The list moderator password must be sha'd
+ key, secret = self.AuthContextInfo(ac)
+ if secret and sha.new(response).hexdigest() == secret:
+ return ac
+ elif ac == mm_cfg.AuthUser:
+ if self.authenticateMember(user, response):
+ return ac
+ else:
+ # What is this context???
+ syslog('error', 'Bad authcontext: %s', ac)
+ raise ValueError, 'Bad authcontext: %s' % ac
+ return mm_cfg.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 must not be None.
+ #
+ # Returns a flag indicating whether authentication succeeded or not.
+ try:
+ for ac in authcontexts:
+ ok = self.CheckCookie(ac, user)
+ if ok:
+ return 1
+ # Check passwords
+ ac = self.Authenticate(authcontexts, response, user)
+ if ac:
+ print self.MakeCookie(ac, user)
+ return 1
+ except Errors.NotAMemberError:
+ pass
+ return 0
+
+ def MakeCookie(self, authcontext, user=None):
+ key, secret = self.AuthContextInfo(authcontext, user)
+ if key is None or secret is None or not isinstance(secret, StringType):
+ raise ValueError
+ # Timestamp
+ issued = int(time.time())
+ # Get a digest of the secret, plus other information.
+ mac = sha.new(secret + `issued`).hexdigest()
+ # Create the cookie object.
+ c = Cookie.SimpleCookie()
+ c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
+ # The path to all Mailman stuff, minus the scheme and host,
+ # i.e. usually the string `/mailman'
+ path = urlparse(self.web_page_url)[2]
+ c[key]['path'] = 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] = ''
+ # The path to all Mailman stuff, minus the scheme and host,
+ # i.e. usually the string `/mailman'
+ path = urlparse(self.web_page_url)[2]
+ c[key]['path'] = 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 0
+ # Treat the cookie data as simple strings, and do application level
+ # decoding as necessary. By using SimpleCookie, we prevent any kind
+ # of security breach due to untrusted cookie data being unpickled
+ # (which is quite unsafe).
+ try:
+ c = Cookie.SimpleCookie(cookiedata)
+ except Cookie.CookieError:
+ return 0
+ # 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 == mm_cfg.AuthUser:
+ if user:
+ usernames = [user]
+ else:
+ usernames = []
+ prefix = self.internal_name() + '+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 1
+ return 0
+ 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.
+ key, secret = self.AuthContextInfo(authcontext, user)
+ if not c.has_key(key) or not isinstance(secret, StringType):
+ return 0
+ # 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].value))
+ issued, received_mac = data
+ except (EOFError, ValueError, TypeError, KeyError):
+ return 0
+ # Make sure the issued timestamp makes sense
+ now = time.time()
+ if now < issued:
+ return 0
+ # Calculate what the mac ought to be based on the cookie's timestamp
+ # and the shared secret.
+ mac = sha.new(secret + `issued`).hexdigest()
+ if mac <> received_mac:
+ return 0
+ # Authenticated!
+ return 1