security
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.
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.
Building Browsergames: forcing users to log in (Perl)
While we’ve been working on building our browsergame, one thing that we haven’t really touched on is making sure that our users are logged in before they try to do anything. Most of the pages that we’ve built rely on the user being logged in so that we can retrieve their User ID and use it for whatever the page does - but none of them force the user to be logged in yet. Today, we’re going to add that piece of functionality to our game.
We already know how to figure out whether or not a user is logged in - all we do is use the values from their cookie, and compare them against the database to see if they match - if they do, we retrieve a User ID. We’ll know a user is logged in when we retrieve something, and we’ll know that they aren’t when we don’t get anything back.
If this sounds familiar, that’s because it is - here’s the code from our index page that does just that:
1 2 3 4 5 6 7 8 9 10 11 12 | my $query = new CGI; my $cookie = $query->cookie('username+password'); my ($username) = split(/\+/,$cookie); use DBI; use config; my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; |
All we need to do is add a quick conditional check to our code to see what was returned, and modify our code slightly so that it’s re-usable in modular format:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | package login; use CGI qw(:cgi); use DBI; use config; my $query = new CGI; my $cookie = $query->cookie('username+password'); my ($username) = split(/\+/,$cookie); my $dbh = DBI->connect("DBI:mysql:$dbname:$dbhost",$dbuser,$dbpass,{RaiseError => 1}); my $sth = $dbh->prepare("SELECT id FROM users WHERE UPPER(username) = UPPER(?)"); $sth->execute($username); my $userID; $sth->bind_columns(\$userID); $sth->fetch; if(!$userID) { print $query->redirect('login.cgi'); } 1; |
If you save that code as login.pm, you’ll now be able add this line to any file that you want to force users to log in to view:
1 | require login; |
And if a user attempts to access the page without their cookie set properly, they’ll be automated redirected to the login page. Check out The index page to see it in action.
Building Browsergames: forcing users to log in (PHP)
While we’ve been building our game, we haven’t really been focusing too much on securing our game against users who haven’t logged in yet. Most of our pages rely on the fact that the user needs to be logged in to see them, and they’ll break horribly if the user isn’t. So today, we’re going to add some handling to our game that will make sure that users are logged in before they try to access something.
You might be wondering how we’re going to figure out whether a user is logged in or not. And the answer to that question is a lot simpler than you might think: we’ll just use what we already have.
Any of our pages that use our stats code have a snippet at the top of them that retrieves the current user’s User ID, so that we can interact with their stats. We can use that code as our starting point - here’s a refresher on what it looks like:
1 2 3 4 5 6 7 8 9 10 | session_start(); 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("SELECT id FROM users WHERE UPPER(username) = UPPER('%s')", mysql_real_escape_string($_SESSION['username'])); $result = mysql_query($query); list($userID) = mysql_fetch_row($result); |
All we do in that code is retrieve the username we stored into session, and then use that value in our SQL to find out what the user’s User ID is. We can easily modify that code, to do a quick check to see what was returned and redirect based on whether or not anything came back:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?php session_start(); 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("SELECT id FROM users WHERE UPPER(username) = UPPER('%s')", mysql_real_escape_string($_SESSION['username'])); $result = mysql_query($query); list($userID) = mysql_fetch_row($result); if(!$userID) { // not logged in! header('Location: login.php'); } ?> |
If you save that file as login-check.php, you can now add this line to any file that you want to require a login for:
1 | require_once 'login-check.php'; |
And if a user attempts to access the page without having logged in first, they’ll be automatically redirected to the login page. Easy!
Cross Site Scripting: what it is, and how to prevent it
Just think: you just created a browsergame. It’s taking off, and people are signing up by the truckload.
You’ve carefully made absolutely sure not to fall victim to SQL Injection, and your launch is going well.
But suddenly, something goes wrong. All of a sudden, users visiting the homepage are automatically getting redirected to nefariouswebsite.com!
What happened?
You’ve just become a victim of what’s known as a Cross Site Scripting attack(or XSS for short). Basically, a user enters some sort of malicious code(like some Javascript) into any area where user-input data will be displayed. It could be as simple as this:
<script type='text/javascript'> window.location.href='http://google.com'; </script>
And the moment that that content gets displayed back to a user, their browser will see the <script> block and then redirect them. In this case, they would only be going to Google - but it’s easy enough to change the URL a user is getting redirected to to anything that you want.
How can you protect against this, if it’s a vulnerability that occurs in any situation where user-input data is displayed?
It’s actually a lot easier than you might have thought. All you need to do is escape all user input that will be displayed. And that means everything.
Do you display the user’s username anywhere? Even if it’s only ever displayed on that specific user’s profile page, you still need to escape it. Even if it’s only ever displayed to the user themselves.
Thankfully, PHP has a function custom-built to handle this - htmlentities(). Perl has a module that you can use to accomplish the same thing - HTML::Entities.
Now, you might be saying “that’s all fine and dandy, but what do I care? It doesn’t affect me if some users are getting redirected from some random page, deep within my site” - but there are multiple problems with that logic:
-
Attacks breed more attacks
Because it’s worked once, an attacker knows it will work again. So there’s no reason not to do it again - and not to do it in a more public place. Attackers perpetrating XSS attacks against sites will sometimes ‘practice’ in a quieter area of the site, so that the site administrators do not become aware of the exploit until it’s too late.
-
User frustration = no more users
Let’s say an XSS attack is executed against your site, and before you can fix it over 1000 users are redirected to nefariouswebsite.com - which just so happens to contain a malicious virus. That’s 1000 users who are never coming back and will never recommend your game ever again. And if you’re unlucky enough, there might be 25 among those 1000 that have decided it’s your fault for not securing your site adequately, and sue you.
Really, there are no gains to be made by not securing your game against XSS attacks. And it’s easy to implement - just make sure htmlentities() (or its equivalent, in your language of choice) is being called on any and all data you retrieve and display.
As long as you escape any and all data that you are going to be displaying to the user, you can avoid the risk of falling prey to an XSS attack. And no matter whether your browsergame is just getting started or already established, that’s always a good thing.
Why you should be hashing sensitive data
When most people think about security, they think about a login/registration system. They usually don’t tend to think about what you need to protect most - a user’s data.
Take, for example, the simple authentication system we built in the last two posts. In both of those cases, there was at least one line of code that would hash the user’s password before it was stored into the database. That’s because, when you’re building something people will be storing their data in, keeping the user’s data safe is vitally important.
There are no ifs, ands, or buts about the situation. If you are storing any kind of sensitive data - be it passwords, credit card numbers, or anything else that can be considered ’sensitive’(even if it’s just the user’s mother’s maiden name or something), you need to either encrypt or hash the data.
What’s the difference between hashing and encryption? There’s just one - encryption can be broken. No matter how strong your encryption is, if someone is determined enough they will eventually manage to break it. Plain and simple.
That’s why, if the data isn’t something you’ll need to retrieve and display, you should hash it instead of encrypt it. By hashing the data, you supply a ’salt’ and the value to hash, and it gets converted one-way into a string of characters. The process of hashing the data makes it irretrievable - if you were to hash, for example, ’23skidoo’, you might get back something like ‘23Kub08nEFeSs’. Whatever data you hash is gone - you can’t get it back.
But even though the hashed data is gone, that doesn’t mean it’s useless - you can still use it for comparisons. As an example, let’s look at some pseudocode for a registration and login system that uses hashing to protect user passwords. Here’s what would happen on the registration page:
if(username not taken and passwords match) {
insert user into database with username sent to us, password(hashed)
}Pretty simple, right? Here’s the logic for the login page:
variable hashedpassword = hash(password sent to us)
if(user exists in database with username passed to us and hashed password) {
log the user in, and redirect them to the post-login page
} else {
login error - password/username mismatch!
}And it’s actually that simple. As long as you don’t need to retrieve the data and display it back to the user, hashed is the way to go for data protection.
Now, you might be thinking “but wait - this is all well and good, but what about if a user forgets their password and I want to e-mail it to them? If I hash their password, I can’t send it to them!” - and you would be right. In that case, you have two options. You could either encrypt their password instead of hashing it, and then just decrypt it and send them that one - or you could simply auto-generate a new password for them, re-hash it and store it into the database, and then send them that new password. Which approach you choose is up to you.
Whatever you do, don’t store sensitive data in plaintext. It only takes one slip for an attacker to get access to your database, and instantly be able to read every user’s passwords. By hashing your data, you can make sure that instead of seeing this:
They see this:
Which will help you to prevent the attacker from actually getting access to any user accounts. Hopefully this brief entry has shown you how easy it is to keep a user’s sensitive data safe - which will help protect both you and your users in the long run.
About
Write for us!
Recent Posts
Categories
- advertising
- askthereaders
- balancing
- buildingbrowsergames
- code
- combat
- cron
- database
- design
- diaryofabrowsergame
- DRY
- frustrations
- games
- gettingstarted
- help
- hosting
- interface
- interview
- javascript
- marketing
- medieval
- monetization
- motivation
- optimization
- perl
- php
- postmortem
- productivity
- publicrelations
- review
- rubyonrails
- security
- setup
- SQL
- strategy
- templates
- terratanks
- tutorial
- Uncategorized
- usability
- workingtitle

