Article | Talk | Edit | History  

MediaWiki Secure login

From the World Wide Wiki

"The movement needs your heart, but it needs your body, too" - Tom Frampton

Jump to: navigation, search

This is a hack I did on Mediawiki to do a simple browser-side encryption of passwords before sending it across the Internet. It's not really a secure login, but it does prevent packet sniffers from seeing your plain text password. It can work just as well (however well that may be) in any other web app, not just media wiki.

So basically I just add an onsubmit Javascript callback to the login form which does an md5 encryption of the contents of the password field, and replaces the contents of that field with the md5 hash (for Javascript md5, see http://pajhome.org.uk/crypt/md5). The thing that gets sent across the lines is the md5 hash, which is theoretically impossible to reverse to get the plaintext password.

For mediawiki, passwords are stored as md5 hashes anyway. Normally, when a user logs in, the plain text password sent in the HTTP submission is encrypted to md5 in the PHP server script, and the hash is compared to the hash stored in the database. So I just changed that code to not encrypt the incoming password, and just compare what was received to what's in the database.

Many mediawiki instances, however, "salt" passwords, apparently as an added measure of security against decrypting a has to a password. Not sure how effective that is, but it's easy to handle for this encrypted password. I'm not going to try to do a "salted" encryption on the browserside, because it requires getting the user id number. There's two implications of this: 1) the browser would need to do a background submission of the username so the PHP can get the user id number and send it back, then it can do the salted encryption and make the final log in submission. 2) the user-id is then being sent out to someone who we haven't verified as being that user (they haven't submitted a password yet, and if we made them do that, it would be unecrypted and entirely defeat the purpose). Not sure if having the user-id is a bad thing or not, but it's probably not a good thing for security purposes.

A salted password just takes the md5 hash of the regular password, prefixes it with the user id number and then a dash, and then stores the md5 hash of that concatenated string. In pseudo code, it's like:

$stored_password_hash = md5 ( $user_id . "-" . md5 ($plainPassword) );

So instead of trying to salt passwords in the browser side, I just check in the PHP log in scrypt to see if the wiki uses salted passwords. If they do, I just "add the salt" to the md5 hash of the plain text password, which was submitted by the browser. In other words, prefix the submitted hash with the user id (PHP can look it up locally in the database) and a dash, and then MD5 the resulting string. The hash of that is the salted password which should be compared to the one stored in the database.

Naturally, this requires some sort of browser side scripting, I used Javascript for it since it's so common. Somewhat ironically, a lot of people have JS disabled for security reasons. Obviously, they won't be able to encrypt their passwords for log in, so if I'm going to allow this (which I am, it only harms them, doesn't really effect the wiki unless it's a sysop or something), I need to know in PHP whether their password is encrypted or not.

To handle all this in a way that will work for Javascript users and Javascript-disabled users, I set up the form assuming they don't have Javascript. I include a checkbox next to the password field, labeled "encrypt". In the HTML, this field is disabled and unset, so by default the password is not encrypted and the user can't change that. However, after the form is loaded, the HTML calls a Javascript which enables and checks that checkbox. Obviously if the user has JS disabled, this won't happen. For those who do have JS, though, they are able to change via the checkbox whether their password gets encrypted or not, and the default is that it does. Also, this Javascript adds the onsubmit callback to the form to do the encryption if the checkbox is checked.

The checkbox gets submitted as part of the form, so PHP can check to see if it was checked or not, and know whether or not the received password is encrypted.

Security

Okay, so what does this actually get you? Basically, if someone is just sniffing packets and you log in without this encryption, they can see your plaintext password and, given the fact that the name of the field in the login form is wgPassword, they're pretty likely to know what that value is. Since a lot of people use the same password for lots of stuff, a snooper who gets your password is likely to start snooping everything you do: find out what sights you go to, and what your username is, etc. Once they know where else you log in, they can try logging in with the same password, so if you've used it more than once, you're leaving yourself open.

So if you encrypt, they don't get your lain text password, which is good. However, it's still not great security, and here's why. First of all, md5 is not flawless, people have been known to reverse it. It's uncommon, but not impossible.

Secondly, no, they don't have your plaintext password, but they do have the md5 hash of your password. Which means they can still break into any site that submits the md5 hash of your password, notably, the one you were logging into when they got your hash.

So it's basically just a thin extra level of security. But it's better than nothing.

APOP style auth

This is something I'd like to add to the encryption scheme, it's similar to the APOP login mechanism for POP3. The idea is to use a nonce (number-user-once) concatenated to the password before hashing.

So this is basically how it would work: The user browses to the log in page. When generating the log in page on the server, the PHP script will generate some unique string. By unique, I mean the string can never be reused for all of eternity on this sever. Sounds like a lot to ask, but if we just use a milli-second timestamp and like a process id or port number kind of thing, it will be fine. This is the nonce, even though it's not necessarily a number. So we generate this string, and send it to the client in some way that it's easy for the browser side script to get (like, in a hidden field in the form).

So the user enters their plaintext username and password as usual into the form, and submits it. On submit, as with the current scheme, the browser side script launches to encrypt the field values before sending. To encrypt the password this time, the password that the user entered in the form has to be prefixed with the nonce which the server sent it. The resulting string is then encrypted with MD5 or whatever, and the resulting hash is sent as the password. Additionally, the nonce will need to be sent back to the server, which can be done by just including it in the hidden fields of the form.

On the server side, the PHP script will basically just do the same thing, take the nonce sent back, and construct it's own concatenation and encrypt it. The resulting hash is compared to the submitted value for authentication.

There's a couple of complications. First, is in general: we need to keep track of the nonce on the server side. If we don't, in other words, if we allow the same nonce to be submitted more than once, then we're right back where we were with the regular md5 encryption: if a snooper gets your nonce and the hash you submitted along with it, then that's all it needs to log in again as you. So basically, for instance, we could setup a database table in MySQL or your DB of choice, which just stores the nonce values which have been generated and sent out, but haven't been responded to. Now, when a login is submitted, you first see if that nonce exists in the table. If it doesn't, that means someone's trying to trick you: either it was never generated, or it was already used. In that case, you reject the login, and send a virus to the computer that made the attack. Otherwise, if the nonce is in the "active nonces" table, then remove it, and do the hash and verification as usual.

You can add some additional security to this, like log the IP address and port that the nonce was sent to, which will basically help verify that it's the same host trying to complete the log in. You should probably also have nonce's expire after a suitable timeout, like twenty minutes or something. For one thing, you can then go through periodically and clean out expired nonces so your table doesn't get too big. Secondly, it's kind of suspicious if a nonce was sent like a year ago, and is just now being used. Worst case scenario, if someone really did just leave their browser open for a year before deciding to log in, just send the login page back again with a new nonce and a message telling them that the login page expired, and they'll have to try again.

The other complication is specific to systems that don't store plain text passwords. Basically, the browser needs to get the password into whatever state the server stores it in (like md5, or md5 with salt, in the mediawiki case), before prepending the nonce and hashing. Otherwise, the server won't be able to reconstruct the hash. For instance, if you take the hash of "mynonce#1-mypassword" and send that to me with "mynonce#1-" as the nonce, but all I know about your password is the md5 hash of it, then I have no way to construct the hash.

If the server stores it in just plain md5 or some other straight forward way (basically, without salt), then it's fine, the browser just encrypts the plaintext password the same way the server stores it, and then does the nonce concetation and hash. However, if, like mediawiki with password salt enabled, it concatenates some user specific value with the password and stores the hash of that, then you're basically looking at a double transaction. First to get the salt for that user name, and then to submit the salt-encrypted login. As mentioned above, this also means that anyone can get anyone else salt, which may well decrease the security of it, I have no idea.