Building Browsergames: Securing our hashes (Perl)

Yesterday, we worked on securing our hashes in PHP – and today, we’re going to take a look at our Perl systems.

According to tilly from perlmonks, crypt() is horribly insecure – which is definitely a big problem. We’re going to need to update our login and registration system, so that our user’s passwords are a little bit more secure if a malicious user ever gained access to our database.

First off, we’re going to convert our code to use the Digest::MD5 module so that we can MD5 our passwords – in addition to salting them. If you don’t have it already, you’ll need to install Digest::MD5. Enter this at your shell(or get your web host to install it for you if they haven’t already):

cpan install Digest::MD5

After installing Digest::MD5, we need to change each line that calls crypt() inside register.cgi and login.cgi. Here’s what the relevant line in register.cgi looks like now:

33
34
			use Digest::MD5 qw(md5);
			$sth->execute($arguments{username},md5('saltgoeshere' . $arguments{password}));

And that’s the only change that you need to make to your register or login code – just adjusting your execute() statements to md5 the password, and making sure that you

use Digest::MD5

in each code file.

However, if you upload your new files and take a look, you’ll notice something – you can no longer log in!

Unfortunately, making this change has broken logins for all of our users who signed up before we had to make this change. Now, if your game hasn’t been released to the public yet or everyone is used to you ‘resetting’ the game, this is fine – but if users are already playing your game, this is a bit of an issue. So we’re going to modify login.cgi, so that it first checks to see whether a user is still using a crypt()’d password or not – and if they are, it will redirect them to a page where they can change their password. Here’s what login.cgi looks like now:

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
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
56
57
58
59
60
61
62
63
64
65
66
#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use CGI::Carp qw(fatalsToBrowser warningsToBrowser);
use DBI;
use config;		# this is our database settings
use HTML::Template;
use Digest::MD5 qw(md5);
 
my $query = new CGI;
my %arguments = $query->Vars;
 
my $template = HTML::Template->new(
		filename	=>	'login.tmpl',
		associate	=>	$query,		# for argument memory
	);
my %parameters;
 
if(%arguments) {
	my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
	my $sth = $dbh->prepare("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
	my $count;
	$sth->execute($arguments{username},crypt($arguments{password},$arguments{username}));
	$sth->bind_columns(\$count);
	$sth->fetch;
	if($count == 1) {
		my $cookie = $query->cookie(
				-name	=>	'username+password',
				-value	=>	$arguments{username} . '+' . crypt($arguments{password},$arguments{username}),
				-expires	=>	'+3M',
			);
		my $uri = 'changepass.cgi';
		print $query->header(-cookie=>$cookie,-location=>$uri);
	} else {
		$sth = $dbh->prepare("SELECT COUNT(id) FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
		$sth->execute($arguments{username},md5('saltgoeshere' . $arguments{password}));
		$sth->bind_columns(\$count);
		$sth->fetch;
		if($count == 1) {
			$sth = $dbh->prepare("UPDATE users SET last_login = NOW() WHERE UPPER(username) = UPPER(?) AND password = ?");
			$sth->execute($arguments{username},md5('saltgoeshere' . $arguments{password}));
			$sth = $dbh->prepare("SELECT is_admin FROM users WHERE UPPER(username) = UPPER(?) AND password = ?");
			my $is_admin;
			$sth->execute($arguments{username},md5('saltgoeshere' . $arguments{password}));
			$sth->bind_columns(\$is_admin);
			$sth->fetch;
			my $cookie = $query->cookie(
					-name	=>	'username+password',
					-value	=>	$arguments{username} . '+' . md5('saltgoeshere' . $arguments{password}),
					-expires	=>	'+3M',
				);
			my $uri = 'index.cgi';
			if($is_admin == 1) {
				# redirect to admin page
				$uri = 'admin.cgi';
			}
			print $query->header(-cookie=>$cookie,-location=>$uri);
		} else {
		$parameters{error} = 'That username and password combination does not match any currently in our database.';
		}
	}
}
 
$template->param(%parameters);
print $query->header(),$template->output();

It might be hard to tell what’s different, there – but take a look at our code to check and see if the user’s information matched anything in our database. We start off by checking the username and password using crypt() – and if it matches anything, we redirect them to changepass.cgi – which we’ll be building shortly. If it doesn’t match with crypt(), we check to see if it matches with Digest::MD5’s md5() function – and if it doesn’t match either of those, we tell the user that it didn’t match anything.

If you haven’t guessed it yet, changepass.cgi is going to be the page users use to change their password. Here’s what the template(changepass.tmpl) 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>
	<tmpl_if name='error'>
		<span style='color:red'>Error: <!--tmpl_var name='error'--></span>
	</tmpl_if>
	<tmpl_if name='message'>
		<span style='color:green'><!--tmpl_var name='message'--></span>
	</tmpl_if>
	<form method='post' action='changepass.cgi'>
		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>

Here’s the code for changepass.cgi:

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
28
29
30
31
32
33
#!/usr/bin/perl -w
 
use strict;
use CGI qw(:cgi);
use DBI;
use config;
use Digest::MD5 qw(md5);
use HTML::Template;
 
require login;
 
my $query = new CGI;
my %arguments = $query->Vars;
my $cookie = $query->cookie('username+password');
my ($username) = split(/\+/,$cookie);
my %params;
 
if(%arguments) {
	if($arguments{password} ne $arguments{confirm}) {
		$params{error} = 'Passwords do not match!';
	} else {
		my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1});
		my $sth = $dbh->prepare("UPDATE users SET password = ? WHERE UPPER(username) = UPPER(?)");
		$sth->execute(md5('saltgoeshere' . $arguments{password}),$username);
		$params{message} = "Password updated successfully.";
	}
}
 
my $template = HTML::Template->new(
		filename	=>	'changepass.tmpl',
	);
$template->param(%params);
print $query->header(), $template->output;

And that’s all there is to it! By making this quick change, we’ve secured our user passwords a little better against any malicious users – and as an added bonus, we’ve created 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.

Wednesday, July 16th, 2008 buildingbrowsergames, code, design, perl, security
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