Port header matching previously described by the misnamed KNONW_SPAMMERS
[mailman.git] / Mailman / SecurityManager.py
blob17cb870cbedb318a88a386ccbe39cbbc97329cbc
1 # Copyright (C) 1998-2007 by the Free Software Foundation, Inc.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
18 """Handle passwords and sanitize approved messages."""
20 # There are current 5 roles defined in Mailman, as codified in Defaults.py:
21 # user, list-creator, list-moderator, list-admin, site-admin.
23 # Here's how we do cookie based authentication.
25 # Each role (see above) has an associated password, which is currently the
26 # only way to authenticate a role (in the future, we'll authenticate a
27 # user and assign users to roles).
29 # Each cookie has the following ingredients: the authorization context's
30 # secret (i.e. the password, and a timestamp. We generate an SHA1 hex
31 # digest of these ingredients, which we call the 'mac'. We then marshal
32 # up a tuple of the timestamp and the mac, hexlify that and return that as
33 # a cookie keyed off the authcontext. Note that authenticating the user
34 # also requires the user's email address to be included in the cookie.
36 # The verification process is done in CheckCookie() below. It extracts
37 # the cookie, unhexlifies and unmarshals the tuple, extracting the
38 # timestamp. Using this, and the shared secret, the mac is calculated,
39 # and it must match the mac passed in the cookie. If so, they're golden,
40 # otherwise, access is denied.
42 # It is still possible for an adversary to attempt to brute force crack
43 # the password if they obtain the cookie, since they can extract the
44 # timestamp and create macs based on password guesses. They never get a
45 # cleartext version of the password though, so security rests on the
46 # difficulty and expense of retrying the cgi dialog for each attempt. It
47 # also relies on the security of SHA1.
49 import os
50 import re
51 import sha
52 import time
53 import urllib
54 import Cookie
55 import logging
56 import marshal
57 import binascii
59 from urlparse import urlparse
61 from Mailman import Defaults
62 from Mailman import Errors
63 from Mailman import Utils
64 from Mailman import passwords
65 from Mailman.configuration import config
67 log = logging.getLogger('mailman.error')
68 dlog = logging.getLogger('mailman.debug')
70 SLASH = '/'
74 class SecurityManager:
75 def AuthContextInfo(self, authcontext, user=None):
76 # authcontext may be one of AuthUser, AuthListModerator,
77 # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator
78 # context.
80 # user is ignored unless authcontext is AuthUser
82 # Return the authcontext's secret and cookie key. If the authcontext
83 # doesn't exist, return the tuple (None, None). If authcontext is
84 # AuthUser, but the user isn't a member of this mailing list, a
85 # NotAMemberError will be raised. If the user's secret is None, raise
86 # a MMBadUserError.
87 key = urllib.quote(self.fqdn_listname) + '+'
88 if authcontext == Defaults.AuthUser:
89 if user is None:
90 # A bad system error
91 raise TypeError('No user supplied for AuthUser context')
92 secret = self.getMemberPassword(user)
93 userdata = urllib.quote(Utils.ObscureEmail(user), safe='')
94 key += 'user+%s' % userdata
95 elif authcontext == Defaults.AuthListModerator:
96 secret = self.mod_password
97 key += 'moderator'
98 elif authcontext == Defaults.AuthListAdmin:
99 secret = self.password
100 key += 'admin'
101 # BAW: AuthCreator
102 elif authcontext == Defaults.AuthSiteAdmin:
103 sitepass = Utils.get_global_password()
104 if config.ALLOW_SITE_ADMIN_COOKIES and sitepass:
105 secret = sitepass
106 key = 'site'
107 else:
108 # BAW: this should probably hand out a site password based
109 # cookie, but that makes me a bit nervous, so just treat site
110 # admin as a list admin since there is currently no site
111 # admin-only functionality.
112 secret = self.password
113 key += 'admin'
114 else:
115 return None, None
116 return key, secret
118 def Authenticate(self, authcontexts, response, user=None):
119 # Given a list of authentication contexts, check to see if the
120 # response matches one of the passwords. authcontexts must be a
121 # sequence, and if it contains the context AuthUser, then the user
122 # argument must not be None.
124 # Return the authcontext from the argument sequence that matches the
125 # response, or UnAuthorized.
126 for ac in authcontexts:
127 if ac == Defaults.AuthCreator:
128 ok = Utils.check_global_password(response, siteadmin=False)
129 if ok:
130 return Defaults.AuthCreator
131 elif ac == Defaults.AuthSiteAdmin:
132 ok = Utils.check_global_password(response)
133 if ok:
134 return Defaults.AuthSiteAdmin
135 elif ac == Defaults.AuthListAdmin:
136 # The password for the list admin and list moderator are not
137 # kept as plain text, but instead as an sha hexdigest. The
138 # response being passed in is plain text, so we need to
139 # digestify it first.
140 key, secret = self.AuthContextInfo(ac)
141 if secret is None:
142 continue
143 if passwords.check_response(secret, response):
144 return ac
145 elif ac == Defaults.AuthListModerator:
146 # The list moderator password must be sha'd
147 key, secret = self.AuthContextInfo(ac)
148 if secret and passwords.check_response(secret, response):
149 return ac
150 elif ac == Defaults.AuthUser:
151 if user is not None:
152 try:
153 if self.authenticateMember(user, response):
154 return ac
155 except Errors.NotAMemberError:
156 pass
157 else:
158 # What is this context???
159 log.error('Bad authcontext: %s', ac)
160 raise ValueError('Bad authcontext: %s' % ac)
161 return Defaults.UnAuthorized
163 def WebAuthenticate(self, authcontexts, response, user=None):
164 # Given a list of authentication contexts, check to see if the cookie
165 # contains a matching authorization, falling back to checking whether
166 # the response matches one of the passwords. authcontexts must be a
167 # sequence, and if it contains the context AuthUser, then the user
168 # argument should not be None.
170 # Returns a flag indicating whether authentication succeeded or not.
171 for ac in authcontexts:
172 ok = self.CheckCookie(ac, user)
173 if ok:
174 return True
175 # Check passwords
176 ac = self.Authenticate(authcontexts, response, user)
177 if ac:
178 print self.MakeCookie(ac, user)
179 return True
180 return False
182 def _cookie_path(self):
183 script_name = os.environ.get('SCRIPT_NAME', '')
184 return SLASH.join(script_name.split(SLASH)[:-1]) + SLASH
186 def MakeCookie(self, authcontext, user=None):
187 key, secret = self.AuthContextInfo(authcontext, user)
188 if key is None or secret is None or not isinstance(secret, basestring):
189 raise ValueError
190 # Timestamp
191 issued = int(time.time())
192 # Get a digest of the secret, plus other information.
193 mac = sha.new(secret + repr(issued)).hexdigest()
194 # Create the cookie object.
195 c = Cookie.SimpleCookie()
196 c[key] = binascii.hexlify(marshal.dumps((issued, mac)))
197 c[key]['path'] = self._cookie_path()
198 # We use session cookies, so don't set 'expires' or 'max-age' keys.
199 # Set the RFC 2109 required header.
200 c[key]['version'] = 1
201 return c
203 def ZapCookie(self, authcontext, user=None):
204 # We can throw away the secret.
205 key, secret = self.AuthContextInfo(authcontext, user)
206 # Logout of the session by zapping the cookie. For safety both set
207 # max-age=0 (as per RFC2109) and set the cookie data to the empty
208 # string.
209 c = Cookie.SimpleCookie()
210 c[key] = ''
211 c[key]['path'] = self._cookie_path()
212 c[key]['max-age'] = 0
213 # Don't set expires=0 here otherwise it'll force a persistent cookie
214 c[key]['version'] = 1
215 return c
217 def CheckCookie(self, authcontext, user=None):
218 # Two results can occur: we return 1 meaning the cookie authentication
219 # succeeded for the authorization context, we return 0 meaning the
220 # authentication failed.
222 # Dig out the cookie data, which better be passed on this cgi
223 # environment variable. If there's no cookie data, we reject the
224 # authentication.
225 cookiedata = os.environ.get('HTTP_COOKIE')
226 if not cookiedata:
227 return False
228 # We can't use the Cookie module here because it isn't liberal in what
229 # it accepts. Feed it a MM2.0 cookie along with a MM2.1 cookie and
230 # you get a CookieError. :(. All we care about is accessing the
231 # cookie data via getitem, so we'll use our own parser, which returns
232 # a dictionary.
233 c = parsecookie(cookiedata)
234 # If the user was not supplied, but the authcontext is AuthUser, we
235 # can try to glean the user address from the cookie key. There may be
236 # more than one matching key (if the user has multiple accounts
237 # subscribed to this list), but any are okay.
238 if authcontext == Defaults.AuthUser:
239 if user:
240 usernames = [user]
241 else:
242 usernames = []
243 prefix = urllib.quote(self.fqdn_listname) + '+user+'
244 for k in c.keys():
245 if k.startswith(prefix):
246 usernames.append(k[len(prefix):])
247 # If any check out, we're golden. Note: '@'s are no longer legal
248 # values in cookie keys.
249 for user in [Utils.UnobscureEmail(u) for u in usernames]:
250 ok = self.__checkone(c, authcontext, user)
251 if ok:
252 return True
253 return False
254 else:
255 return self.__checkone(c, authcontext, user)
257 def __checkone(self, c, authcontext, user):
258 # Do the guts of the cookie check, for one authcontext/user
259 # combination.
260 try:
261 key, secret = self.AuthContextInfo(authcontext, user)
262 except Errors.NotAMemberError:
263 return False
264 if key not in c or not isinstance(secret, basestring):
265 return False
266 # Undo the encoding we performed in MakeCookie() above. BAW: I
267 # believe this is safe from exploit because marshal can't be forced to
268 # load recursive data structures, and it can't be forced to execute
269 # any unexpected code. The worst that can happen is that either the
270 # client will have provided us bogus data, in which case we'll get one
271 # of the caught exceptions, or marshal format will have changed, in
272 # which case, the cookie decoding will fail. In either case, we'll
273 # simply request reauthorization, resulting in a new cookie being
274 # returned to the client.
275 try:
276 data = marshal.loads(binascii.unhexlify(c[key]))
277 issued, received_mac = data
278 except (EOFError, ValueError, TypeError, KeyError):
279 return False
280 # Make sure the issued timestamp makes sense
281 now = time.time()
282 if now < issued:
283 return False
284 # Calculate what the mac ought to be based on the cookie's timestamp
285 # and the shared secret.
286 mac = sha.new(secret + repr(issued)).hexdigest()
287 if mac <> received_mac:
288 return False
289 # Authenticated!
290 return True
294 splitter = re.compile(';\s*')
296 def parsecookie(s):
297 c = {}
298 for line in s.splitlines():
299 for p in splitter.split(line):
300 try:
301 k, v = p.split('=', 1)
302 except ValueError:
303 pass
304 else:
305 c[k] = v
306 return c