Cookie security hole in admin interface

[Didn't see this problem discussed in the recent archive messages, so...]
I was looking at the code for the admin cgi in search of a good cookie authentication system, and found out that it was doing this,
c = Cookie.Cookie( os.environ['HTTP_COOKIE'] )
if c.has_key(list_name + "-admin"):
if c[list_name + "-admin"].value == hash(list_name)
:
return 1
...to authenticate based on a cookie. This code is from 1.0b8, but it only took a couple of minutes to set the appropriate wafer in my junkbuster configuration, and point netscape at the admin page for mailman-developers. I'll leave the replication of this exploit as an exercise for the readers.
Possible solutions:
Lock down that url with whatever security features your web server has. This sucks as a long term solution, but it should protect from disgruntled script kiddies that you just chucked off your lists.
Make the value based on a hash of some slow changing system variable. Something that changes with the frequency of your desired expire time, for example. Maybe a cron job to set a key based on some fast changing system stats every hour or so.
Use SSL for the admin interface and save the name and password in the cookie.
Any better suggestions?
John.

John Morton writes:
I was looking at the code for the admin cgi in search of a good cookie authentication system, and found out that it was doing this,
[etc]
Any better suggestions?
A quick glance at the WWW security FAQ suggests a good solution:
http://www.w3.org/Security/Faq/wwwsf7.html#Q66
John.

[John Morton]
John Morton writes:
I was looking at the code for the admin cgi in search of a good cookie authentication system, and found out that it was doing this,
Thanks for letting us know -- this certainly gave me some incentive to have a look at what those pesky cookies really are all about. I guess Barry'll have to whip up a new release (1.0rc2?) shortly...
Any better suggestions?
A quick glance at the WWW security FAQ suggests a good solution:
As the extra complexity added by having to save session state on the server side (i.e. have Mailman keep track of session IDs) is rather large, and as Mailman isn't safe from package sniffing anyway (unless you're running things on a SSL server, in which case cookie sniffing shouldn't be of any trouble anyway), I settled for slightly less.
I have just commited a fix to CVS, based on these two new SecurityManager functions:
def MakeCookie(self):
client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0'
issued = int(time.time())
expires = issued + mm_cfg.ADMIN_COOKIE_LIFE
secret = self.password
mac = hash(secret + client_ip + `issued` + `expires`)
return [client_ip, issued, expires, mac]
def CheckCookie(self, cookie):
if type(cookie) <> type([]):
return 0
if len(cookie) <> 4:
return 0
client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0'
[for_ip, issued, expires, received_mac] = cookie
if for_ip <> client_ip:
return 0
now = time.time()
if not issued < now < expires:
return 0
secret = self.password
mac = hash(secret + client_ip + `issued` + `expires`)
if mac <> received_mac:
return 0
return 1
Hopefully, this new cookie scheme will suffice -- if anyone do see flaws in it, don't hesitate to get in touch.
Harald

Harald Meland writes:
As the extra complexity added by having to save session state on the server side (i.e. have Mailman keep track of session IDs) is rather large, and as Mailman isn't safe from package sniffing anyway (unless you're running things on a SSL server, in which case cookie sniffing shouldn't be of any trouble anyway), I settled for slightly less.
True. Though stealing a cookie via packet sniffing will still require the thief to be on the same IP as the original cookie owner, or it will require them to fake their IP as well. This definitely makes the list vulnerable to only an extremely determined attacker.
[Note, I'm not a python hacker, so bear with me :-) ]
Do add the domain that this mailman for this list is supposed to be under (or the browser will send this cookie to every site you connect to!) and restrict the scripts that it is sent to to /mailman/ at worst, /mailman/admin and /mailman/admindb separately at best (allowing separate passwords for those two scripts is a needed feature for a proper distinction between list admins and moderators, anyway).
Of course, you could already be doing this and I just missed it :-)
I have just commited a fix to CVS, based on these two new SecurityManager functions:
def MakeCookie(self): client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0' issued = int(time.time()) expires = issued + mm_cfg.ADMIN_COOKIE_LIFE secret = self.password
I'd prefer to grab the secret from a file that's refreshed via a cronjob every 24hrs or so. Generate it from something like a CRC32 of some /dev/urandom output (on a linux box, anyway) would do the trick.
mac = hash(secret + client_ip + `issued` + `expires`)
Is the hash() function one way? How about:
import md5 import base64
mac = base64.encodestring(md5.new(secret + client_ip + issued
+ expires
).digest())
The FAQ talks about doing a second round of MD5 hashing to '...avoid an attack in which additional data is appended to the end of the cookie and a new hash recalculated by the attacker.', but I don't really understand why that's necessary.
With any luck, someone will have implemented an HMAC (see RFC 2104, http://www.it.kth.se/docs/rfc/rfcs/rfc2104.txt) module for python that we could use.
return [client_ip, issued, expires, mac] def CheckCookie(self, cookie): if type(cookie) <> type([]): return 0 if len(cookie) <> 4: return 0 client_ip = os.environ.get('REMOTE_ADDR') or '0.0.0.0' [for_ip, issued, expires, received_mac] = cookie if for_ip <> client_ip: return 0 now = time.time() if not issued < now < expires: return 0
Should we check that expires - issued = mm_cfg.ADMIN_COOKIE_LIFE ?
secret = self.password mac = hash(secret + client_ip + `issued` + `expires`)
See above.
if mac <> received_mac: return 0 return 1
Hopefully, this new cookie scheme will suffice -- if anyone do see flaws in it, don't hesitate to get in touch.
The combination of a cryptographic hashing function and a secret key that's regenerated on a 24hour cycle makes an attack by constructing a cookie infeasible (at least, within the useful lifetime of whatever hashing function we're using).
Remaining sources of attacks are:
Packet sniffing: They could steal the cookie this way. But they'd just steal the password, anyway, so it's a moot point. Use SSL if you're that paranoid.
Stealing the cookie: Presumably by some method other than packet sniffing or direct access to your terminal; maybe a broken browser could be distributing the cookie to every site it meets. The attacker will need to fake your IP to the web server _and_ get the response back, which is rather hard.
Getting access to your terminal: The short expiry time is supposed to help defeat this problem. The price to pay for the convenience of not having to type the password in every time.
Perhaps having an option to turn off the use of cookies will keep the paranoid happy and allow admins to use their servers native authentication methods. Perhaps modules to interface between mailman and the various different web servers is a direction someone would like to go in?
John.

Harald Meland writes:
As the extra complexity added by having to save session state on the server side (i.e. have Mailman keep track of session IDs) is rather large, and [...]
In a local CGI application, we are storing cookies in an LDAP server which would be an excellent supplement for Mailman anyway. User database and some other things might be stored there. I toyed around with that idea in conjunction with our old Listprocessor but gave up on that because the Listprocessor is such a mess.
+gg
-- Gerhard.Gonter@wu-wien.ac.at Fax: +43/1/31336/702 g.gonter@ieee.org Zentrum fuer Informatikdienste, Wirtschaftsuniversitaet Wien, Austria
participants (3)
-
Gerhard Gonter
-
Harald Meland
-
John Morton