Proposed new Tutorial on Login Scripts for review
Posted: Thu Apr 05, 2012 7:27 pm
Thanks to Celauran and social_experiment for all their work in writing this comprehensive tutorial, which is now posted in the Tutorials forum: viewtopic.php?f=28&t=135287. If you have further questions or suggestions, please direct them to the authors. This thread is now locked.
DevNet members Celauran and social_experiment have collaborated on this tutorial and I am only publishing it here for peer review, at Christopher's suggestion, so that anyone who wishes to propose changes or enhancements may do so, before posting it in our Tutorials forum. They have provided sample scripts for login and registration for those using mysqli or PDO, as well, in the attached archive file. If you have suggestions, either post here or PM the authors. In about a week or whenever the authors let me know they are ready, I'll post it as a sticky tutorial.
Here is the body of the Tutorial. The additional files are in the attachment:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Creating login systems can be a challenge for people who are new to PHP. For their benefit we decided to create an article showing some points to look at when creating a login system. These are common pitfalls identified by looking at previous questions from newbies who attempted to write login systems but got stuck somewhere along the way. This may cause some to continue using the poorly crafted scripts, only to realize later (possibly after a cracking attempt) that the script wasn't properly secured.
Because this is aimed at less experienced users, a few things to point out before starting:
1. Any page that uses sessions has to have the session_start() function at the top (before any other output, except the php opening tag) of that specific page.
If you have 5 pages to protect, all 5 pages needs session_start() at the top.
2. When using mysql_ or mysqli_ functions, you need to make a connection to the database. Certain mysqli_ functions require the use of a resource returned by mysqli_connect(). Throughout this article the presence of a database connection is assumed.
Pitfall 1: Storing passwords
The short answer is don't. You don't store passwords anywhere. Ever. Why? If your database is ever compromised, then every user account is also compromised. A list of usernames, passwords, and possibly email addresses in the hands of an attacker can be disastrous. Still, for a user to access your site, you need to validate the credentials they provide at login against those they provided at registration. The solution, then, is to use hashes. Store a hash of the password in your database, hash the password provided at login, and compare the hashes. Because hashing algorithms are one-way, computing the hash of any given value is trivial, but working backwards from the hash to the original value is impossible.
Still, not all hashing algorithms are created equal. For example, many tutorials -- and indeed some published works -- advocate the use of the md5 hashing algorithm for creating password hashes. While this may once have been acceptable practice, this is no longer the case. More on that later. For now, to create a reasonably secure working hash, we'll create a salt, create a pepper, combine these with the user's password, and finally hash with something like the sha384 algorithm (minimum). Salts are user-specific and can be stored in the database, while peppers are site-wide and often reside in a file somewhere or as a string which has various characters and is at least 30 characters long.
One key point to remember here is that you aren't trying to protect against unauthorized logins to your site; you're trying to prevent passwords from being discovered in the event that your user database is compromised. To that end, you want to employ a slow hashing algorithm. Read why. Take 5 minutes to read that article. Go ahead, I'll wait. You can use bcrypt directly by calling the crypt() function with a suitable salt, or you can take advantage of PHPass which uses bcrypt when available and degrades gracefully where it isn't. They also have an excellent article on hashing and user authentication.
Example of how you can create a hashed value
Alternately, using PHPass
It's worth noting that any given algorithm will produce hashes of the same length regardless of the length of the input. These will often be considerably longer than the length of the password being hashed. Be sure to check the length of the output of your algorithm of choice and ensure that the password field in your database is sufficiently long to store the entire hash.
Pitfall 2: Not escaping input
Quite a few example scripts portray a query against the database in the following manner:
The $_POST values above are taken directly from the form, without any checking of any sort. Input should always be treated as if it is contaminated. Before using data in a database query, it needs to be validated and escaped.
You can use mysql_real_escape_string() for this purpose if you are using mysql or MySQLi if you are using mysqli (which you really ought to be using) has a similar function. Better still, make use of prepared statements. Regardless of which method you ultimately choose, the value of properly escaping your data cannot be overstated. Relevant.
mysql_real_escape_string() accepts an argument that is to be escaped making it safe to use in a database query. The explanation from the PHP Manual on the function
Some scripts rely on the magic_quotes_gpc setting to determine whether or not to use mysql_real_escape_string(); though you could check for the existence of the value, it is wise to note that from PHP 5.3.0 the feature is deprecated (and the function shouldn't be used anymore). The above code snippet can be amended as follows:
Pitfall 3: Session vulnerabilities
Sessions are commonly used to distinguish authenticated users from the unauthenticated. Typically, in processing a login form, you'll see something like this
Unfortunately, this leaves you vulnerable to session fixation (PDF) attacks. As captured session IDs are generally worthless unless the user is signed in, you can protect against this by regenerating the session ID upon successful login. Session data is written only once this new ID has been generated.
To additionally protect against session hijacking, add some sort of signature to the session. A combination of user ID, User-Agent, and some random salt should suffice. So we update the above to include this signature.
Of course, any page in a protected area is going to check that the requesting user has been authenticated, often using code similar to this.
Effectively myusername can be empty and the auth script would see this as "logged in". The variable is registered but a better option would be to fill it with something concrete to check against. This is where our signature comes into play. By ensuring that user ID is present, loggedIn is true, and recalculating the signature and ensuring it matches what's stored in session data, we can be reasonably certain we're dealing with a legitimate user.
We hope that this has been of use to those who don't know where to start when writing a login system. For those who are experienced in the matter please add any comments, critique or additional tips so the article can be as complete as possible.
DevNet members Celauran and social_experiment have collaborated on this tutorial and I am only publishing it here for peer review, at Christopher's suggestion, so that anyone who wishes to propose changes or enhancements may do so, before posting it in our Tutorials forum. They have provided sample scripts for login and registration for those using mysqli or PDO, as well, in the attached archive file. If you have suggestions, either post here or PM the authors. In about a week or whenever the authors let me know they are ready, I'll post it as a sticky tutorial.
Here is the body of the Tutorial. The additional files are in the attachment:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Creating login systems can be a challenge for people who are new to PHP. For their benefit we decided to create an article showing some points to look at when creating a login system. These are common pitfalls identified by looking at previous questions from newbies who attempted to write login systems but got stuck somewhere along the way. This may cause some to continue using the poorly crafted scripts, only to realize later (possibly after a cracking attempt) that the script wasn't properly secured.
Because this is aimed at less experienced users, a few things to point out before starting:
1. Any page that uses sessions has to have the session_start() function at the top (before any other output, except the php opening tag) of that specific page.
Code: Select all
<?php
session_start();
// rest of code
?>2. When using mysql_ or mysqli_ functions, you need to make a connection to the database. Certain mysqli_ functions require the use of a resource returned by mysqli_connect(). Throughout this article the presence of a database connection is assumed.
Pitfall 1: Storing passwords
The short answer is don't. You don't store passwords anywhere. Ever. Why? If your database is ever compromised, then every user account is also compromised. A list of usernames, passwords, and possibly email addresses in the hands of an attacker can be disastrous. Still, for a user to access your site, you need to validate the credentials they provide at login against those they provided at registration. The solution, then, is to use hashes. Store a hash of the password in your database, hash the password provided at login, and compare the hashes. Because hashing algorithms are one-way, computing the hash of any given value is trivial, but working backwards from the hash to the original value is impossible.
Still, not all hashing algorithms are created equal. For example, many tutorials -- and indeed some published works -- advocate the use of the md5 hashing algorithm for creating password hashes. While this may once have been acceptable practice, this is no longer the case. More on that later. For now, to create a reasonably secure working hash, we'll create a salt, create a pepper, combine these with the user's password, and finally hash with something like the sha384 algorithm (minimum). Salts are user-specific and can be stored in the database, while peppers are site-wide and often reside in a file somewhere or as a string which has various characters and is at least 30 characters long.
One key point to remember here is that you aren't trying to protect against unauthorized logins to your site; you're trying to prevent passwords from being discovered in the event that your user database is compromised. To that end, you want to employ a slow hashing algorithm. Read why. Take 5 minutes to read that article. Go ahead, I'll wait. You can use bcrypt directly by calling the crypt() function with a suitable salt, or you can take advantage of PHPass which uses bcrypt when available and degrades gracefully where it isn't. They also have an excellent article on hashing and user authentication.
Example of how you can create a hashed value
Code: Select all
<?php
$hashedValue = hash('sha384', $salt.$pepper.$password);
// returns a 96 character string which is stored in the database
?>Code: Select all
$hasher = new PasswordHash(8, FALSE);
$hash = $hasher->HashPassword($password);Pitfall 2: Not escaping input
Quite a few example scripts portray a query against the database in the following manner:
Code: Select all
<?php
$username = $_POST['username'];
$email = $_POST['email'];
$qry = "SELECT * FROM Table WHERE username='$username' and email='$email'";
?>You can use mysql_real_escape_string() for this purpose if you are using mysql or MySQLi if you are using mysqli (which you really ought to be using) has a similar function. Better still, make use of prepared statements. Regardless of which method you ultimately choose, the value of properly escaping your data cannot be overstated. Relevant.
mysql_real_escape_string() accepts an argument that is to be escaped making it safe to use in a database query. The explanation from the PHP Manual on the function
Depending on personal requirements you should also check for empty fields, certain types of characters, certain types of data. Checking the data you receive is just as important as escaping it. Use existing php functions such as trim(), ctype functions, filters, or regular expressions to ensure that input is valid and of the type expected. User input is NEVER to be trusted.PHP Manual wrote:Escapes special characters in the unescaped_string, taking into account the current character set of the connection so that it is safe to place it in a mysql_query(). If binary data is to be inserted, this function must be used. mysql_real_escape_string() calls MySQL's library function mysql_real_escape_string, which prepends backslashes to the following characters: \x00, \n, \r, \, ', " and \x1a.
This function must always (with few exceptions) be used to make data safe before sending a query to MySQL.
Some scripts rely on the magic_quotes_gpc setting to determine whether or not to use mysql_real_escape_string(); though you could check for the existence of the value, it is wise to note that from PHP 5.3.0 the feature is deprecated (and the function shouldn't be used anymore). The above code snippet can be amended as follows:
Code: Select all
<?php
// no checking of data; improve this by using existing or custom
// functions.
$username = mysql_real_escape_string($_POST['username']);
$email = mysql_real_escape_string($_POST['email']);
$qry = "SELECT `columnA`, `columnB` FROM `TableName` WHERE `username`='$username' and `email`='$email'";
?>Sessions are commonly used to distinguish authenticated users from the unauthenticated. Typically, in processing a login form, you'll see something like this
Code: Select all
<?php
if ($rows==1)
{
header("location:/login_success.php");
}
else
{
echo "Wrong Username or Password";
}
?>Code: Select all
<?php
if($rows==1)
{
session_regenerate_id();
$_SESSION['user_id'] = $user_id;
$_SESSION['loggedIn'] = true;
// close the session
session_write_close();
header("location:/login_success.php");
exit();
}
else
{
echo "Wrong Username or Password";
}
?>Code: Select all
<?php
if($rows==1)
{
session_regenerate_id();
$_SESSION['user_id'] = $user_id;
$_SESSION['loggedIn'] = true;
$_SESSION['signature'] = md5($user_id . $_SERVER['HTTP_USER_AGENT'] . $salt);
// close the session
session_write_close();
header("location:/login_success.php");
exit();
}
else
{
echo "Wrong Username or Password";
}
?>Code: Select all
<?php
if (!session_is_registered(myusername))
{
header("location:mainlogin.php");
}
?>Code: Select all
if (!isset($_SESSION['user_id']) || !isset($_SESSION['signature']) || !isset($_SESSION['loggedIn']) || $_SESSION['loggedIn'] != true || $_SESSION['signature'] != md5($_SESSION['user_id'] . $_SERVER['HTTP_USER_AGENT'] . $salt))
{
session_destroy();
header("Location: mainlogin.php");
exit();
}