Building Browsergames: Securing our hashes (PHP)

John Munsch recently pointed out that there’s a bit of a glaring security hole in our login and registration systems: at the moment, we’re extremely vulnerable to Rainbow Table attacks. In John’s words:

The MD5 hash doesn’t actually protect you if someone were able to dump your table of users or gain access to the database in some fashion.

And you know what? He’s absolutely right. If a malicious user managed to get access to our database at the moment, our user’s logins wouldn’t be protected at all. This is a big problem, and something we need to fix.

Unfortunately, because password hashing is one-way, we can’t just get users to reset their password. They’ll either need to re-register entirely, or we can setup a special page(and stat) in order to make sure that users have reset their passwords. John recommends adding what’s known as a ’salt’ value to user’s passwords – that way, you might have something like this:

password = 'foo'
password + salt = 'foobrownfox'
hashed password = hash('foobrownfox')

And if a user were to attack our login information using a rainbow table, they might manage to figure out that the passwords being stored in the database were values like ‘foobrownfox’ – but they’d have a bit of a harder time figuring out what was the salt value and what was the actual password.

Luckily, this is a pretty easy fix to implement – we just modify our two calls to md5() in our login and register code to add a salt to the user’s passwords. Here’s the changes we make to register.php:

24
				mysql_real_escape_string(md5('saltgoeshere' . $password)));

Unfortunately, at this moment making these changes breaks things for users who signed up before we had to make this fix. In order to try and keep things as seamless as possible for the user, we’ll be modifying our login code slightly:

20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
	$query = sprintf("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER('%s') AND password='%s'",
		mysql_real_escape_string($username),
		mysql_real_escape_string(md5($password)));
	$result = mysql_query($query);
	list($count) = mysql_fetch_row($result);
	if($count == 1) {
		$_SESSION['authenticated'] = true;
		$_SESSION['username'] = $username;
		header('Location:changepass.php');
	} else {
		$query = sprintf("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER('%s') AND password='%s'",
			mysql_real_escape_string($username),
			mysql_real_escape_string(md5('saltgoeshere' . $password)));
		$result = mysql_query($query);
		list($count) = mysql_fetch_row($result);
		if($count == 1) {
			$_SESSION['authenticated'] = true;
			$_SESSION['username'] = $username;
			$query = sprintf("UPDATE users SET last_login = NOW() WHERE UPPER(username) = UPPER('%s') AND password = '%s'",
				mysql_real_escape_string($username),
				mysql_real_escape_string(md5('saltgoeshere' . $password)));
			mysql_query($query);
			$query = sprintf("SELECT is_admin FROM users WHERE UPPER(username) = UPPER('%s') AND password='%s'",
				mysql_real_escape_string($username),
				mysql_real_escape_string(md5('saltgoeshere' . $password)));
			$result = mysql_query($query);
			list($is_admin) = mysql_fetch_row($result);
			if($is_admin == 1) {
				header('Location:admin.php');			
			} else {
				header('Location:index.php');				
			}
		} else {	
			$error = 'There is no username/password combination like that in the database.';
		}
	}

We’ve made a small change to our login code, so that it first tests to see if the user’s attributes match up to any users who haven’t had their passwords salted – if they do, we redirect them to the page where they can change their password. Here’s what the template for our ‘change password’ page(change_pass.tpl) looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head>
	<title>Change Password</title>
</head>
<body>
	{if $error ne ""}
		<span style='color:red'>Error: {$error}</span>
	{/if}
	{if $message ne ""}
		<span style='color:green'>{$message}</span>
	{/if}
	<form method='post' action='changepass.php'>
		Password: <input type='password' name='password' id='password' /><br />
		Confirm Password: <input type='password' name='confirm' /><br />
		<input type='submit' value='Change Password' />
	</form>
	<script type='text/javascript'>
	document.getElementById('password').focus();
	</script>
</body>
</html>

With the template created, all we need to do is build the page that handles changing the user’s password – like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
 
include 'smarty.php';
require_once 'login-check.php';
 
if($_POST) {
	$password = $_POST['password'];
	$confirm = $_POST['confirm'];
	if($password != $confirm) {
		$error = 'Passwords do not match!';	
	} else {
		require_once 'config.php';		// our database settings
		$conn = mysql_connect($dbhost,$dbuser,$dbpass)
			or die('Error connecting to mysql');
		mysql_select_db($dbname);
		$query = sprintf("UPDATE users SET password = '%s' WHERE username = '%s'",
					mysql_real_escape_string(md5('saltgoeshere' . $_POST['password'])),
					mysql_real_escape_string($_SESSION['username']));
		mysql_query($query);
		$message = 'Password updated successfully.';
	}	
}
$smarty->assign('error',$error);
$smarty->assign('message',$message);
$smarty->display('change_pass.tpl');
 
?>

And that’s all there is to it! With a fairly simple change, we’ve managed to secure our user’s information a bit better – a malicious user with direct access to our database won’t be able to easily figure out what a user’s password is just by using a Rainbow table. As an added bonus, we’ve also created an extra piece of functionality – a change password page!

Note: don’t forget to change ’saltgoeshere’ to an actually random value, like ’s79dj@#*(hd’ or something – you won’t make any security gains if malicious users can easily guess your password salt. If you’re feeling really adventurous, you could(and probably should) turn the salt into a configuration parameter – but I’ll leave how to do that up to you.

Wish there was more?

I'm considering writing an ebook - click here.

.

Luke is the primary editor of Building Browsergames, and has written a large portion of the articles that you read here. He generally has no idea what to say when asked to write about himself in the third person.

Tuesday, July 15th, 2008 buildingbrowsergames, code, design, php, security
  • ValerioDeCamillis

    Well, this post is 2 years old now, but still for those reading it now: this is not the proper way of storing salted hashes.

    The real purpose of salt is to render hashes of the same password different from each other so that if two users have the same password their hashed values would still look different.

    A proper implementation would add a random salt generated on the fly for each user, and store that value in a 'salt' column in the db.

    Then you can compute the proper hash fetching the salt associated with the username, then joining it with the password the user typed in.

    Disqus is too laggy today for me to write proper code, but i doubt anyone will be reading this anyway.

  • MrLollige

    you could(and probably should) turn the salt into a configuration parameter

    Why? I mean, if I change the salt value (if someone figured it and modified his dictionary to it), noone would be able to login any more....

  • The benefits of turning it into a configuration value aren't so much in
    securing it, as they are in not repeating it everywhere - if your salt is
    'thequickbrownfoxjumpedoverthelazydog', do you really want to type that
    everytime you need it?

  • MrLollige

    Ah ok :).
    Anyway, my salt (which I made before I read this thanks to the user comments on other pages) is short, and I probably do not need it anywhere else than on the login and register page.
    Thanks for your reply!

  • It doesn't matter much, but it isn't necessary to nest the md5() function within mysql_real_escape_string() as you will never have to escape a hexadecimal string.

    While it usually works regardless, HTTP/1.1 requires you to use an absolute URL in header redirects. Example from php manual below:

    /* Redirect to a different page in the current directory that was requested */
    $host = $_SERVER['HTTP_HOST'];
    $uri = rtrim(dirname($_SERVER['PHP_SELF']), '/\\');
    $extra = 'mypage.php';
    header("Location: http://$host$uri/$extra");
    exit();

blog comments powered by Disqus

About

Building Browsergames is a blog about browsergames(also known as PBBG's). It's geared towards the beginner to intermediate developer who has an interest in building their own browsergame.

Sponsors

Got Something to Say?

Send an e-mail to luke@buildingbrowsergames.com, or get in touch through Twitter at http://twitter.com/bbrowsergames