Preparing a secure login form with PHP & JavaScript
Wed, 26th Dec, '07
We have had encryption, we have had SSLs, … well we also had digitally signed certificates, but where’s the hack?? How can you cook up a secure login form that does the following:
[1] doesn’t send the login information in clear-text
[2] in case somebody is sniffing the line, he/she shouldn’t be able to login with the sniffed information
So, with the above information in hand, here is what we do, and how we do.
You need:
[1] http://www.webtoolkit.info/javascript-md5.html [javascript implementation of MD5]
[2] Two php functions. runquery($query) , which will run a query supplied as string & getcol($query) will get the column asked for in a select statement.
Now, create a table, if you don’t already have, to store user information. What we do want to be stored is the login timestamp.
create table user(
loginid varchar(200),
password varchar(200),
lastLoginTS bigint
);
Now your login.php file should look like this:
<html>
<head>
<title>Secure Login Form</title>
<script type=”text/javascript” src=”md5.js”></script>
</head>
<body>
<form action=”dologin.php” method=”post” onsubmit=”javascript:document.getElementById(‘phash’).value = MD5(document.getElementById(‘password’).value + document.getElementById(‘hts’).value); document.getElementById(‘password’).value = ” ;”>
LoginID: <input type=”text” name=”loginid”><br>
Password: <input type=”password” name=”password” id=”password”><br>
<?php
$TS = time(); //the current timestamp
echo “<input type=’hidden’ value='”.$TS.”‘ name=’hts’ id=’hts’><br>”;
?>
<input type=’hidden’ name=’phash’ id=’phash’ value=”>
<input type=”submit” value=”send”>
</form>
</body>
</html>
This file assumes in the line “<script type=”text/javascript” src=”md5.js”></script>” that you have a file named md5.js in the same directory as login.php, and the javascript file should have a function named MD5(). So make sure this is the case. Next up is dologin.php:
<?php
$loginid = $_REQUEST[‘loginid’];
$phash = $_REQUEST[‘phash’];
$hts = $_REQUEST[‘hts’];
$password = getcolumn(“select password from user where loginid=’$loginid’;”);
$lastLoginTS = getcolumn(“select lastLoginTS from user where loginid=’$loginid’;”);
if(strlen($loginid) > 0 && strlen($phash) > 0 && $phash == md5($password.$hts) && $hts > $lastLoginTS)
{
runquery(“update user set lastLoginTS='”.time().”‘ where loginid=’$loginid’;”);
echo “done”;
}
else
echo “failed”;
?>
That’s it. So now what is the deal here. This is your regular login form except that the password is hashed with a timestamp value sent in a hidden form field named ‘hts’. The hashing is done in the event handler for the Javascript onsubmit event, and the password field is cleared as well, to prevent it from being sent in the clear.
The server receives the loginid, timestamp and the hashed value from the client. Retrieve from your database the original password for the loginid specified and calculate another hash at the server with the help of the retrieved original password and the timestamp sent by the client. If the user typed the password correctly, the hashes will match.
This method of course, sort of, encrypts the password and hence prevents the password from being sent in clear, should anybody be sniffing the lines. But should anybody be really sniffing the lines, he/she can just store the values and send them again and again to validate him/herself at the server posing as the valid user. To prevent that, there is another check at the server just before validating. The timestamp sent by the client should be always greater than the last login timestamp stored in the database for that user. Since the last login timestamp is only updated on a successful login, as soon as a valid user logs in, the last login timestamp for the user is updated in the database, and as a result, the sniffed information is rendered stale. The hash now needs to be calculated again using a fresh timestamp and a password which only the user and server know.
I hope this suffices for most of you out there.
UPDATE : This is a proof of concept. The system described here lacks certain things which are very obvious and shouldn’t omit them just because I haven’t mentioned them here to make it simple to grasp. Foremost[thanks William], don’t store passwords in cleartext on the server. Try looking up “hashing password with salts” for that.
Wed, 26th Dec, '07 at 21:10
That does not seem like a lot of code. Will test this out.
Thanks
Wed, 26th Dec, '07 at 21:43
Ok.
First of all!!! Great!! 🙂
Can you extend this further so that it can be implemented for registration page too!! In case of registration page, if i go for the same technique and a sniffer sniffs out the password hash, everything else would then be rendered stale, beacuse the sniffer has the original password now!!!!
Can we make it work for registration too???
Thu, 27th Dec, '07 at 3:00
This is definitely not for registration since this will only work if the password is known at both ends beforehand. For Signup pages, you can use any of the available encryption methods but please do check up the legal implications before finalizing on any particular encryption procedure.
Thu, 27th Dec, '07 at 22:17
Its a really bad idea to store unhashed passwords in a database. If that database gets compromised (even if its read only access) then the compromiser will know everyone passwords. An obvious example of why this is bad is most people re-use passwords, but I don’t feel like making a formal argument, so just google and you’ll find why you should only be storing hashed passwords.
Thu, 27th Dec, '07 at 22:36
Very true William. And I will add a UPDATE to the post because it struck me later on that some people might lap it up as it is presented.
Fri, 22nd Feb, '08 at 21:03
I resolved the hashed password issue. My database actually stores MD5’ed passwords. Here is how I did:
The javascript calculates the password’s MD5. Then it concatenates the result with the timestamp and calculates the MD5 again. The result is sent to the server.
The server reads the MD5’ed password from the database, concatenates the timestamp and calculates the MD5.
Now compare the received hash with the calculated one and here you are!
I guess all this MD5 thing is a simple way not to send plain text passwords over the web, but it still is quite vulnerable. A clever eavesdrop reading the POST request can figure out how all this is done and, later, try to obtain the hashed password with a bruteforce loop:
1. a = string to test
2. a += timestamp given in the POST
3. b = MD5(a)
4. if (b == password given in the POST) { bingo! }
5. goto 1
I guess some days or weeks would be enough to break the password, probably less with a dictionary-based loop first.
Fri, 22nd Feb, '08 at 21:06
Ops, I did not thank you for the idea! I just implemented it and feel a deep relief not to sent plain text passwords anymore.
Thanks (:
Sat, 23rd Feb, '08 at 6:34
Hi Gabriel, and am really happy to see you taking attention and even going as far as adopting it.
About security, well, I have given up on the idea of a future-proof security device or method. The best I can do is make a parrot learn vowels and pass messages through it. How would you know which parrot to trap?? “Don’t get anybody curious” might be the way to it, but I have other things to do right now. :).
Tue, 11th Mar, '08 at 12:49
[…] No Translations This article discusses a way for HTML login forms not to transmit plain-text passwords over the internet when SSL or https are too complex and still in your TODO list. This is an enhancement of this solution. […]
Mon, 28th Apr, '08 at 13:56
Hi,
Thanks again for the info shared.
I fell a “BIG” thing is missing here.
What if the sniffer sends $hts greater than the current time. he will be successful in logging into the system.
may be u have that in ur mind but dint tell it out..
” system described here lacks certain things which are very obvious”
Thanks for letting me know .. if I got it completely wrong !
Mon, 28th Apr, '08 at 14:13
Hi vijay,
you can always send the hashing timestamp greater than the current time, but remember that you also have to hash the password with the timestamp. for the attack to succeed you need to know the password and hash it correctly. if you know the password already, you would rather use the login form and then nothing can stop you. am i right in getting your point here??
Mon, 28th Apr, '08 at 14:29
Thanks Rahul,
You are correct and I am such a jerk ! 😦
Tue, 26th Aug, '08 at 8:24
I really like the idea. However one would have to combine it with HTTPS (preferably with a certificate signed by a well-known and trusted entity) to prevent a possible man-in-the-middle attack.
Otherwise the man-in-the-middle could just remove the java-script code and then do the hashing before sending the request on to the real server.
But then again, when using HTTPS with a trusted certificate, is there really any need to send a hashcode instead of the verbatim-but-now-SSL-encrypted password?
Hmmm…
And I see another possible problem: what about overly-restrictive content-filters that strip java-script? A page crippled by such filters would still send the password in cleartext. The server could of course recognize such a case by inspecting the “password” and “phash” fields, but the password would still be on the wire in cleartext.
Tue, 26th Aug, '08 at 10:06
@paul: I had put this up as an alternative to simple login systems, the based ones. This definitely is not a replacement for HTTPS. And the setup required to really do a man-in-the-middle against this is fairly demanding and simple listening won’t do.
And thanks for stopping by, its very lonely out here.
Sat, 17th Jan, '09 at 4:04
[…] ‚Preparing a secure login form with PHP & JavaScript‘ by Kumar Rahul (observances): Beschreibt die Clientseitige Verschlüsselung des Passwortes mit MD5 (SHA-1 wäre auch möglich), „gesalzen“ mit einem Timestamp aus PHP::time() […]