1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
|
# Copyright (C) 1998,1999,2000,2001 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, raise a
# MMNotAMemberError error. 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' % 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:
# 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:
# 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 crypt and crypt.crypt(response, secret[:2]) == 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:
# The user's passwords are kept in plain text
key, secret = self.AuthContextInfo(ac, user)
if secret and response == secret:
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.MMNotAMemberError:
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 Errors.MMBadUserError
# Timestamp
issued = int(time.time())
# Get a digest of the secret, plus other information.
mac = sha.new(secret + `issued`).hexdigest()
# Create the cookie object. The way the cookie module converts
# non-strings to pickles can cause problems if the resulting string
# needs to be quoted. So we'll do the conversion ourselves.
c = Cookie.Cookie()
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.Cookie()
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
c = Cookie.Cookie(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 == 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
for user 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
try:
data = marshal.loads(binascii.unhexlify(c[key].value))
issued, received_mac = data
except (EOFError, ValueError, TypeError):
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
|