http://www.perlmonks.org?node_id=153931

With the recent...um...events surrounding tye, there has been a good deal of discussion about password management for sites such as PM. In light of that, I'm going to describe the technique I've used (and plan to use again) in hopes of spreading enlightenment, as well as resolving any flaws in the technique.

The setup

Passwords are stored encrypted via Crypt::PasswdMD5 or some other equivalent module. I chose this one because it seems perfectly suited to the task and because there are javascript implementations of it (I'll explain why shortly). This is done to prevent the passwords being exposed if the database is compromised in some form, either by hack or by tye (sorry couldn't resist).

There are two ways in which the login process can be handled. Basically you have to prevent the username/password from being transmitted in the clear across the net. Any black hat packet sniffing could catch it.

  1. Make the pages that handle the login process only be accessible under SSL. This is easy and straight forward to implement and works. The downside is you have to pay the CA (certificate authority) tax, or live with the annoying 'do you trust this site?' dialog.
  2. Don't send the password in the clear in the first place. There are several different Javascript implementations of the PasswdMD5 algorithm, you can use this to encrypt the password BEFORE it is send to the server.

The encrypted password is validated against the database and if they match a Apache::Session is created for this user. A cookie is passed back to the browser that contains the session id. No other cookies will be used. Into the Apache::Session goes the uid of the user, the IP from which they connected, and the time stamp. Since the user has no way to access the contents of the apache::session, you can consider this data untainted.

The reason for storing the IP of the host that they connect from is to prevent the session from being hijacked if the cookie can be obtained via packet sniffing. Each time that the cookie is passed back, the connected host's IP is compared against the one in the session (which was set at the time of the successful login), if they don't match then it's been hijacked.

The time stamp is used for an inactivity timeout...to prevent the 'I left myself logged in while I went to lunch and somebody sat down at my computer and sold my body on eBay' syndrome.

Issue: Changing password

Alteration of the password is done Unix style. The page that the authenticated user accesses contains 3 fields. current password, new password and new password (again). They enter their current password and the desired new one twice (to make sure there are no typos), if they enter the correct current one, and the 2 new ones are the same the password is changed. This page follows the same security rules as the login page...it's either SSL or the passwords are encrypted before they are sent. The reason for prompting for the current password is to prevent the scenario where the person *JUST* got up from there computer and someone ran in before the session expired. This also would prevent a malicious user from changing the password if they do manage to hijack the session somehow. Also the new password is fed through a password strength checker (Unix has these, I'm sure there's a CPAN mod for it). What good is a secure password scheme if luser 'john' set's his password to 'John'?

Issue: I forgot my password

Since the passwords are stored encrypted, then you can't very well send it to them. So, the password can be reset to a new random one using Crypt::RandPasswd and emailed to the email address stored in the database for user john. If we have the public PGP key for 'john' then we PGP encrypt the message. (it does no good to email the password in the clear if there's a black hat sniffing traffic).

This opens the door for a DOS attack via the 'forgot my password' link, or someone else resetting john's password for him.

  • The DOS part is solved by adding a 'LastForgotDate' field to john's data and only allowing his password to be reset once a day.
  • The malicious change part can be solved by adding another set to the process. Once john enters his username on the forgot password page, he's present with another screen that asks for the answer to a personal question ie, "what is your mother's maiden name?". If the correct answer is given then 'john's password is reset (john supplied the question and answer as part of the registration process).

Issue: I forgot my password AND the email address I registered with is non-accessible

This is one I'm not sure how to handle...I've never run across this problem and it very well may be a fringe condition that just requires calling up the sysadmin and getting him/her to reset it (using the secret Q/A like the change password does)

Wrap up

So, if we were storing all this info in a relational database, then the user table would contain at least
id database generated numeric id
username the username
password PasswdMD5 encrypted password
email email address to send reset password to
lastresetdate date of last password reset (prevents DOS attack)
question secret question to be asked if the password needs to be reset
answer answer to the question (used to prevent malicious password resets)
pgpkey user's public PGP key (to prevent the 'here is your new password email from being sniffed)

The questions my fellow monks are: How can this system be defeated? What problems does it have? How can it be improved?

/\/\averick

Replies are listed 'Best First'.
Re: Web based password management (or how *not* to blame tye)
by belg4mit (Prior) on Mar 24, 2002 at 20:47 UTC
    Passing the password as an MD5 hash isn't any better than passing it in the clear, if it weren't done over SSL. Just thought I'd point it out and make it explicit.

    I've done something similar in the past. If we wanted to be truly paranoid we'd implement S/Key. (I wish I had my JavaScript S/Key implementation working, maybe someday...).

    UPDATE: Some reading on S/Key; RFC 1938, RFC 2289

    --
    perl -pe "s/\b;([st])/'\1/mg"

      um...ya..duh. Pardon the blonde moment. I was thinking of a different scheme and combined two. The javascript md5 thing would work if you sent along a random salt into the login page, then the password (or the md5 crypted password) is crypted with this salt and then sent to the server. Thus capturing it wouldn't do any good, since to login again, there would be a different salt.

      Better?

      /\/\averick
      perl -l -e "eval pack('h*','072796e6470272f2c5f2c5166756279636b672');"

        Yeah, that would be closer to what I did in the past ;-) I used the session ID as the salt. And reset the session on failed attempts.

        --
        perl -pe "s/\b;([st])/'\1/mg"

Re: Web based password management (or how *not* to blame tye)
by cjf (Parson) on Mar 24, 2002 at 21:00 UTC
    How can this system be defeated?

    I can think of two ways: customers and management.

    One of the big requirements of your system is that all your users have a PGP key. Most likely many of them won't, and many won't even know what one is. This will lose you business, and management tends not to like lost business. That said, you have to consider that security is a tradeoff. Will encrypting emailed passwords save you more money in security incidents than it will cost you in lost business? Things like consumer confidence in your company are hard to attach a dollar sign to, but most companies will try nonetheless.

    The other security vulnerability is the customer themselves. If they do have a PGP key, but they pay no attention to security on their home computer, their account is still vulnerable. Modeling security threats with attack trees by Bruce Schneier is an excellent article that focuses on this concept. Also of limited vulnerability would be the users selected question. It could be easy to guess, or the attacker could convince the person on the phone they had made up a nonsense question and didn't know the answer.

    All in all it's a good system. In a real-world environment you will have to make some security compromises in favor of usability, the hard part is deciding how far to take these compromises.

Re: Web based password management (or how *not* to blame tye)
by aersoy (Scribe) on Mar 24, 2002 at 22:56 UTC
    Since the passwords are stored encrypted, then you can't very well send it to them. So, the password can be reset to a new random one using Crypt::RandPasswd and emailed to the email address stored in the database for user john. If we have the public PGP key for 'john' then we PGP encrypt the message. (it does no good to email the password in the clear if there's a black hat sniffing traffic).

    Do not do that! Really, never send a cleartext password to an email address. IF you have their public PGP key, then it can be applicable, but not otherwise.

    If you don't have the key, try this instead: Ask for a login name or an email address. Make sure it exists in your database. If it's not an email, get the email associated with it from your records. Create a temporary, rather long random key and save it somewhere, along with the data they entered. Send them an email and ask them to go to an URL like this: http://www.example.com/reset_pass?key=<random_key>. That page will hold a simple form to enter a username (or email, in case username can be forgotten, too) and a new password, twice. When they submit that, compare the key with the one you saved and take action if, and only if, those keys match.

    This way, you can avoid sending passwords in clear case (well, partially). Plus, the password you create can be quite complex, thus make the user type it rather slowly. I can usually guess what people type just by looking at their fingers, and it's really easy if you know the keyboard well and they don't. This kind of thievery will be avoided, also.

    And for the last issue, I myself would not try to automate this, too. I think it needs to be handled in person. Ask other questions along with the one in the database, if applicable (ie. 'when did you first create the account', 'when did you last logged in'. If this information is public, then they are no use, of course.) If the answer is accurate, then you can consider changing the email address in the database with the new one and ask for a new password. Otherwise, it's best asking them to simply create a new account.

    --
    Alper Ersoy

Re: Web based password management (or how *not* to blame tye)
by Dog and Pony (Priest) on Mar 24, 2002 at 21:09 UTC
    What you really would like is something like UNIX's crypt or some similar in javascript. The implementation is known, so it wouldn't be impossible, but I think it would be quite lengthy... which is a bad thing on the web. :)

    Another approach that I have seen is that you randomly generate a password the first time too, and email that to the user. That way you can use better encryptions - but you face the same problems with non-encrypted email - if that is an issue, and without the javascript thingy, the user can't change his password.

    The best solution should of course be to encrypt on the serverside and use SSL - but that has the drawbacks you mention above.

    When retreiving lost passwords: Depending on what kind of users you have, you could possibly have the user that lost the password enter his/her email address instead, which will then be matched to a user and emailed just like above. Reason for this is, that on many sites, the usernames might be known, but not the email addresses. So the black hat dude would have to figure out a valid email first, and then succeed in sniffing it (unless PGP is also on, in which case it hardly matters). Drawback is that some users tend to forget what email they signed up with...

    And of course, as people will point out, demanding javascript will shut some out, and make others angry. But if that is the rules of the site - hey it is your site after all, and personally I wouldn't mind at all if it had good reasons.

    Some good thoughts there, but it would seem that for tight security, there is no good replacement for encrypting all the traffic, which has lots of penalties in performance, price etc.

    One also always must ask oneself what is a reasonable security level for the particular site, and weigh risk/gain against each other.


    You have moved into a dark place.
    It is pitch black. You are likely to be eaten by a grue.
Re: Web based password management (or how *not* to blame tye)
by derby (Abbot) on Mar 24, 2002 at 20:52 UTC
    maverick,

    ++. Just one question.

    the IP from which they connected

    What happens if the user is sitting behind a pool of proxies? Do you run the risk of "false-positive" hijacked session?

    -derby

      In my experience, yes you do. :)

      I worked on a web application that started by comparing whole IP addresses on each access, and we started to have quite a few reports of people behind proxy pools having a problem.

      Backing up a bit and only checking to see if the IP is in the same /16 or /24 (checking the first two or three numbers, that is) helps, although it doesn't eliminate the problem entirely (and it really weakens the effectiveness of the test).

      Checking IPs can be useful in some situations, but for large-scale applications where the "general public" will be connecting to your interface, I wouldn't recommend it.

      - Matt Riffle

        I worked on a web application that started by comparing whole IP addresses on each access, and we started to have quite a few reports of people behind proxy pools having a problem

        Really? I figured this sort of thing would be a very fringe condition. I figured that most people weren't behind proxies at all, and that of the proxied people, most only had one. Then of those that had multiples, the auto-proxy configuration script would pick one of the pool at random, or round robin. The concept of sending different requests from the same browser through different proxies seems counter productive to efficient caching...

        /\/\averick
        perl -l -e "eval pack('h*','072796e6470272f2c5f2c5166756279636b672');"

Check the cookie for changes
by drewbie (Chaplain) on Mar 25, 2002 at 15:05 UTC
    I have one thing to add. When sending any data that is persistent (like a cookie) to the client, you should ALWAYS include a hash (like MD5 or SHA1) of the original value so you can easily see if the cookie value has been modified.

    Also, you should use cryptographically sound session ids to lessen the chance of guessing a valid session. I came across a very good paper recently that talked about this. The URL is http://www.usenix.org/events/sec01/fu/fu_html/index.html.

      The session ids that I'm using here are those generated by Apache::Session itself. They appear to be a md5 hash of some sort (I've not really looked into *how* they are generated). That's the only piece of info ever stored in a cookie, and validating if the cookie has been tinkered with is easy.
      eval { # tie to session_id (sorry it's monday morning don't remember the +exact syntax) } if ($@) { print "bogus session\n"; }
      basically if you ask apache session to access a session that doesn't exist it dies. Thus the eval...

      /\/\averick
      perl -l -e "eval pack('h*','072796e6470272f2c5f2c5166756279636b672');"

        True, that would work fine if all you're storing in the cookie is the session id. I usually put a little more in the cookie than just the id, so using a hash to verify the value I put there is second nature to me.

        Here's a scenario: User A get a cookie w/ the session and logs into a web app w/ sensitive data. User B has access to User A's computer (hacker, social engineering, etc) and gets the session ID. User B then creates a session cookie like User A's, and now he can see the sensitive data he should not have access to. Using a checksum on the cookie value can help to avoid situations like this. And there really is not a downside. You write the code once, it uses C based modules so it's fast, and you prevent one less possible security problem. Maybe you'll never run across this situation, but should you do so you don't have to worry.

        Just a thought...

        The default, Apache::Session::Generate::MD5, does MD5->hexhash(time(). {}. rand(). $$)/ Systems such as ASP under IIS appear to have session ID's that are partially determined by the client headers the (User-agent, Accept, etc.)

        PS> I was horrified re-reading the Apache::Session documentation that in the SYNOPSIS they place a cc number in the session data.

        --
        perl -pe "s/\b;([st])/'\1/mg"

      When sending any data that is persistent (like a cookie) to the client, you should ALWAYS include a hash (like MD5 or SHA1) of the original value so you can easily see if the cookie value has been modified.
      Anyone who modifies the cookie can also recompute the hash value so that it matches. To prevent that, you need to include secret information in the hash itself -- see Digest::HMAC. If you go that route, you can eliminate the need to store session information on the web server at all.
      $cookie = join(",", $user_name, $time, $remote_ip); $cookie .= "," . unpack("H20", hmac_sha1($cookie, $secret));
      (I cut the hash down to 80 bits, because that should be enough to prevent a brute-force key search.) Then your only problem is keeping $secret synchronized, if you have a pool of load-balanced servers. Verifying the cookie against both the current secret and the previous secret should give you some leeway so the servers don't all have to be updated at the same instant. It'll also avoid invalidating everybody's current logins when you change the secret.

      Update: Yes, that's basically what I meant, drewbie. HMAC is a construction that avoids some possible weaknesses when using a hash as a MAC.

      Possible Objection: An attacker who breaks into the web server and learns $secret can log in as anyone.

      Response: He can probably forge himself a new Apache::Session, too. Or trojan the login cgi so it saves the passwords in a log file.

        Anyone who modifies the cookie can also recompute the hash value so that it matches. To prevent that, you need to include secret information in the hash itself -- see Digest::HMAC. If you go that route, you can eliminate the need to store session information on the web server at all.

        I'm not sure if we're talking about the same thing here or not. The hash I mentioned is a MAC of the cookie value you are sending along w/ a secret string stored on the server. This MAC does prevent the attacker from regenerating the cookie. A good example of this in Apache::TicketTool::make_ticket discussed in the mod_perl book (Writing Apache Modules in Perl and C by Stein & MacEachern) page 317.

        So I think we're talking about the same thing. I haven't had time to finish reading RFC 2104 (which Digest::HMAC implements), so I can't be sure. But I know the method I mentioned has been widely discussed as reasonably secure.