califdon wrote:Nothing wrong with collaboration
Agreed; i am not a security specialist but if i can impart a bit of the knowledge i have picked up during my development years so be it.
Below my attempt
If you are an experienced user of php, writing a login script comes pretty easy. Not only the code part but the logic behind it. Often a php newbie (term used in the most sincere form) will attempt a login system for whatever reason. There are lots of tutorials available on the internet but the things new users tend to do is to accept these as ‘a complete and totally secure login script’ when infact they are (as authors often state) only examples which should serve as a jump off point on how to write login systems.This post also does not offer a silver bullet solution as there is none in these situations; there is only sound practise. As technology changes so may the validity techniques described here.
Because being new to this is confusing I will attempt to explain as clearly as possible. I make no claims about my knowledge on this subject, I’m adding to and combining points made by various users on the forum. Use this article as a guideline when creating a login script, always ask questions about security and if in doubt post your question on the PHPDN forum > Security section.
How the article is structured
Throughout the article I list the pitfall, give a small explanation about it and directly afterwards an alternative to the problem. If a code snippet is present it is related to the text it is located beneath. Questions, comments, improvements, things I missed, etc are all welcome.
I make the assumption that you wish to create a login system using PHP, an SQL database, have some page/s that you don’t want anyone accept a registered (and logged in) user to see. Database connections are also implied.
Pitfall 1: How do I store my password in the database?
To successfully login a user you need to do validation – Validate the credentials you get against the details you have in the database. The first step to a more secure login system is to NEVER store the password in the database, not in any form. The reason is simple: If your database is compromised, every account that you have is compromised.
The only option to consider is to store a hashed value consisting not of the password but of the password plus other 'ingredients'. How you create the hashed value is up to you, but I think one thing that most will agree on is that it shouldn’t be easy (near impossible) to determine the plain-text should the hashed value be obtained.
Many examples (and sadly some published books) advocate the use of md5() as a hashing algorithm. Md5() is not a safe hashing algorithm and shouldn’t be used for this purpose. Create a salt, create a pepper then combined these with the password; then hash with something like the sha384 algorithm (minimum). Salts can be stored in the database while peppers often reside in a file somewhere or as a string which has various characters and is at least 30 characters long.
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
?>
Pitfall 2: Escaping input
Quite a few example scripts portrays a query against the database in the following manner:
Code: Select all
<?php
$username = $_POST['username'];
$password = $_POST['password'];
$qry = "SELECT * FROM Table WHERE username='$username' and password='$password'";
?>
The $_POST values above are taken from the form; without any checking of any sort. Input should always be treated as if it is contaminated. To use data in a database query it has to be escaped. Use
mysql_real_escape_string() for this purpose.
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
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.
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() or regular expressions to check input. User input is NEVER to be trusted.
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']);
$password = mysql_real_escape_string($_POST['password']);
$qry = "SELECT * FROM Table WHERE username='$username' and password='$password'";
?>
Pitfall 3: Not regenerating a session after validation
This snippet will login a user if a match for the credentials are found but it leaves the script vulnerable to session based attacks.
Code: Select all
<?php
if($rows==1) {
header("location:/login_success.php");
}
else {
echo "Wrong Username or Password";
}
?>
Calling the
session_regenerate_id() function will update the existing session id to a new value. After this is done any session values that will be used are declared. A preferred option to session_register() for registering session variables is the method below:
Code: Select all
<?php
if($rows==1) {
session_regenerate_id();
$_SESSION['username'] = $username;
$_SESSION['loggedIn'] = true;
// close the session
session_write_close();
header("location:/login_success.php");
exit();
}
else {
echo "Wrong Username or Password";
}
?>
Two types of session based attacks are session fixation and session hijacking. The following from the wikipedia pages on each type of attack
Wikipedia wrote:
session fixation attacks attempt to exploit the vulnerability of a system which allows one person to fixate (set) another person's session identifier (SID).
Wikipedia wrote:
session hijacking is the exploitation of a valid computer session—sometimes also called a session key—to gain unauthorized access to information or services in a computer system
session_regenerate_id() changes the existing session_id value to a different on that was issued at the start of the script; a different one to the one an attacker might have.
Pitfall 4: Checking if a visitor is logged in
I refer to code that checks if a user is logged in as 'auth' code. This code goes on each page that the user wants protected and make accessible to registered / logged in users only.
The following snippet is an example of auth code present in a first time ‘auth’ page
Code: Select all
<?php
if(!session_is_registered(myusername)) {
header("location:mainlogin.php");
}
?>
Apart from the fact that the session_is_registered() function that shouldn’t be used, this code doesn’t check the value inside the variable; from the PHP Manual
PHP Manual wrote:
session_is_registered() returns TRUE if there is a global variable with the name name registered in the current session, FALSE otherwise.
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.To protect the page you need values that you can check against; a custom session id, a value in a database, a variable set on login. As with the password hashing option the amount of techniques that can be used here is abundant; personal preference comes into play so I will give a basic idea of what to implement
Code: Select all
session_start();
if (!isset($_SESSION['custom_sessid']) ||$_SESSION['custom_sessid'] == 0 ||
$_SESSION['loggedIn'] == false) {
header("location: login.php");
exit();
The snippet above checks if the session variable has not been set, if the id is equal to 0 or if the loggedId session variable is false. The key thing in here is the OR operator (||) which means that if any of the above conditions are true the browser will redirect the user atttempting to view the page.
Many options to check exists when it comes to 'auth' code but stick to values that you can set yourself; ip's and browser types can be spoofed so relying on them isn't advisable.
And that's my take on login script basics. One thing i would like to point out is the use of mysqli_ functions which should rank above mysql_ counterparts. I used the mysql_ option as it was present in the majority of the posts i searched through and i think mysqli_ will give new users a bit of an uphill battle at first. I noticed this in an earlier post by Celauran
The use of PDO is also recommended once you get the hang the login script basics and how to use prepared statements to improve interactions with the database.