SECURING PHP USER AUTHENTICATION, LOGIN, AND SESSIONS

SECURING PHP USER AUTHENTICATION, LOGIN, AND SESSIONS

Many, if not all, of you have had to deal with creating a secure site login at some point in time. Although there are numerous articles written on the subject it is painstakingly difficult to find useful information from a single source. For this reason I will be discussing various techniques I have used or come across in the past for increasing session security to hinder both session hijacking and brute force password cracking using Rainbow tables or online tools such as GData. I use the word hinder due to the fact no foolproof methods exist for preventing session hijacking or brute force cracking, merely increasing degrees of difficulty. Choose a method wisely based on your site’s current or anticipated traffic, security concerns, and intended site usage. The following examples have been coded using PHP and MySQL. I more than willingly accept comments, suggestions, critiques, and code samples from readers like you as they benefit the community on the whole.

Update: Security Concerns with Hashing Algorithms

There are some inherent security considerations to take into account when using very fast hashing algorithms such as SHA or MD5. Modern day, multi-processor computers and GPUs can quickly brute-force passwords that aren’t encrypted with a very slow, secure algorithm. For these reasons, it is recommended you do not use these and instead use bcrypt encryption or sha-256/512 with key stretching. In the near future there will be a post containing this updated method for secure authentication.

The One-Way Password Hashing Algorithm

123456789101112131415161718192021222324252627282930313233343536373839404142
<?php
/**
* Generates a secure, pseudo-random password with a safe fallback.
*/
function pseudo_rand($length) {
if (function_exists('openssl_random_pseudo_bytes')) {
$is_strong = false;
$rand = openssl_random_pseudo_bytes($length, $is_strong);
if ($is_strong === true) return $rand;
}
$rand = '';
$sha = '';
for ($i = 0; $i < $length; $i++) {
$sha = hash('sha256', $sha . mt_rand());
$chr = mt_rand(0, 62);
$rand .= chr(hexdec($sha[$chr] . $sha[$chr + 1]));
}
return $rand;
}
/**
* Creates a very secure hash. Uses blowfish by default with a fallback on SHA512.
*/
function create_hash($string, &$salt = '', $stretch_cost = 10) {
$salt = pseudo_rand(128);
$salt = substr(str_replace('+', '.', base64_encode($salt)), 0, 22);
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return crypt($string, '$2a$' . $stretch_cost . '$' . $salt);
}
return _create_hash($string, $salt);
}
/**
* Fall-back SHA512 hashing algorithm with stretching.
*/
function _create_hash($password, $salt) {
$hash = '';
for ($i = 0; $i < 20000; $i++) {
$hash = hash('sha512', $hash . $salt . $password);
}
return $hash;
}

The password validation function

123456789101112
<?php
/**
* @param string $pass The user submitted password
* @param string $hashed_pass The hashed password pulled from the database
* @param string $salt The salt used to generate the encrypted password
*/
function validateLogin($pass, $hashed_pass, $salt) {
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return ($hashed_pass === crypt($pass, $hashed_pass);
}
return ($hashed_pass === _create_hash($pass, $salt));
}
Below this line are older, obsolete methods for securing your data. I do not recommend using the password hashing methods outlined below, but the general ideas and concepts still apply.

Method 1: Hashed Password, Unique Salt

The first method involves storing a unique salt in one of your configuration files, a define statement, or class constant. Salting your passwords prior to hashing (MD5SHA1SHA256Whirpool, etc.) hinders such attacks by increasing the amount of storage and computation required to crack your password. For a full listing of supported hashing algorithms, take a look at hash_algos. The primary concern with a singular unique salt for any number of stored passwords is that once figured out, the salt becomes utterly useless.

Part 1: The MySQL Table

1234567891011
CREATE TABLE `secure_login` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(120) NOT NULL,
`password` VARCHAR(40) NOT NULL,
`session` VARCHAR(40) DEFAULT NULL,
`disabled` TINYINT(1) UNSIGNED DEFAULT 0,
`created_dt` DATETIME DEFAULT '0000-00-00 00:00:00',
`modified_ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `uniq_idx` (`email`)
) ENGINE=InnoDB CHARSET=UTF8;
view rawone-way-mysql.sql hosted with ❤ by GitHub

Part 2: The One-Way Password Hashing Algorithm

123456789
<?php
define('UNIQUE_SALT', '5&nL*dF4');
function create_hash($string, $hash_method = 'sha1') {
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return hash($hash_method, UNIQUE_SALT . $string);
}
return sha1(UNIQUE_SALT . $string);
}

Part 3: The password validation function

1234567891011121314
<?php
define('UNIQUE_SALT', '5&nL*dF4');
/**
* @param string $pass The user submitted password
* @param string $hashed_pass The hashed password pulled from the database
* @param string $hash_method The hashing method used to generate the hashed password
*/
function validateLogin($pass, $hashed_pass, $hash_method = 'sha1') {
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return ($hashed_pass === hash($hash_method, UNIQUE_SALT . $pass));
}
return ($hashed_pass === sha1($hash_method, UNIQUE_SALT . $pass));
}

Method 2: Hashed Password, Random Salt

The second method utilizes a more secure form of salt, the random salt. The random salt is generated upon account creation and is unique to that account. The benefit of using random salts is that a compromised account will have no adverse effect on the remainder of accounts due to the uniqueness of the salts on a per user basis. A good salt should incorporate letters, numbers, and symbols and preferably be over 8 characters in length.
Although this method is still more secure than the first, it may have a negative impact of requiring you to perform an extra database query to grab secure data after validating the session. The first query performs a lookup of the uid, password, and salt based on either a username or email address. The second query would generally be performed if you required further data that was not contained within your user login table. This query would more likely utilize a join on related related tables to gather further information. Benefits of utilizing a random salt

Part 1: The MySQL Table

123456789101112
CREATE secure_login (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(120) NOT NULL,
`salt` VARCHAR(8) NOT NULL,
`password` VARCHAR(40) NOT NULL,
`session` VARCHAR(40) DEFAULT NULL,
`disabled` TINYINT(1) UNSIGNED DEFAULT 0,
`created_dt` DATETIME DEFAULT '0000-00-00 00:00:00',
`modified_ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `uniq_idx` (`email`)
) ENGINE=InnoDB CHARSET=UTF8;
view rawone-way-mysql.sql hosted with ❤ by GitHub

Part 2: The pseudo-random salt generator

12345678910
<?php
function randomSalt($len = 8) {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()-=_+';
$l = strlen($chars) - 1;
$str = '';
for ($i = 0; $i &lt; $len; ++$i) {
$str .= $chars[rand(0, $l];
}
return $str;
}

Part 3: The One-Way Password Hashing Algorithm

123456789
<?php
function create_hash($string, $hash_method = 'sha1', $salt_length = 8) {
// generate random salt
$salt = randomSalt($salt_length);
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return hash($hash_method, $salt . $string);
}
return sha1($salt . $string);
}

Part 4: The Password Validation Function

12345678910111213
<?php
/**
* @param string $pass The user submitted password
* @param string $hashed_pass The hashed password pulled from the database
* @param string $salt The salt pulled from the database
* @param string $hash_method The hashing method used to generate the hashed password
*/
function validateLogin($pass, $hashed_pass, $salt, $hash_method = 'sha1') {
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return ($hashed_pass === hash($hash_method, $salt . $pass));
}
return ($hashed_pass === sha1($salt . $pass));
}

Method 3: Hashed Password, UNIX Timestamp Based Salt

Assuming your database becomes compromised, it would be very easy for an attacker to piece together your encryption algorithm if your user login table contains a field named salt. This method provides an abstracted way of hashing your password based on a substring of the last modified date of your user login entry. You may consider storing your user’s created date as a TIMESTAMP as opposed to an UNSIGNED INT because of the Year 2037 Bug. This may seem a little ridiculous to assume my PHP scripts will still be running in 2037, but then again nobody readily anticipated the Y2K Bug either. The benefit of using a pre-existing, non-changing field in your user table as a salt is that it adds obscurity. If your database is compromised, it is not readily apparent that your passwords are salted and there is no indication of how they were salted. If you do choose to go the UNSIGNED INT route with your user’s createed date, I recommend doing some obfuscation of the date as your salt (i.e. using every odd digit, reversing the integer, etc.).

Part 1: The MySQL Table

12345678910111213
CREATE secure_login (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`email` VARCHAR(120) NOT NULL,
`salt` VARCHAR(8) NOT NULL,
`password` VARCHAR(40) NOT NULL,
`session` VARCHAR(40) DEFAULT NULL,
`disabled` TINYINT(1) UNSIGNED DEFAULT 0,
# your hidden salt will be the reverse of the created_dt value
`created_dt` INT(11) UNSIGNED,
`modified_ts` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE INDEX `uniq_idx` (`email`)
) ENGINE=InnoDB CHARSET=UTF8;
view rawlogin-table.sql hosted with ❤ by GitHub

Part 2: The One-Way Password Hashing Algorithm

12345678910111213
<?php
/**
* created_date must be a valid date() formatted string
*/
function create_hash($string, $created_date, $hash_method = 'sha1') {
// the salt will be the reverse of the user's created date
// in seconds since the epoch
$salt = strrev(date('U', strtotime($created_date));
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return hash($hash_method, $salt.$string);
}
return sha1($salt.$string);
}
view rawpassword-hashing.php hosted with ❤ by GitHub

Part 3: The Password Validation Function

1234567891011121314
<?php
/**
* @param string $pass The user submitted password
* @param string $hashed_pass The hashed password pulled from the database
* @param string $created_date The user's created date pulled from the database
* @param string $hash_method The hashing method used to generate the hashed password
*/
function validateLogin($pass, $hashed_pass, $created_date, $hash_method = 'sha1') {
$salt = strrev(date('U', strtotime($created_date));
if (function_exists('hash') && in_array($hash_method, hash_algos()) {
return ($hashed_pass === hash($hash_method, $salt . $pass));
}
return ($hashed_pass === sha1($salt . $pass));
}
view rawlogin-validation.php hosted with ❤ by GitHub

IP Address Validation

IP address validation relies on the fact that the majority of users will maintain a static IP over the duration of their site visit. On each page load we would be performing a comparison for equality on the visitor’s current IP and the stored copy of their IP in the session. Drawbacks from this method are related to the fact that many individuals do not have a static IP. Some ISP providers lease IP addresses for a given amount of time before expiration (generally for the duration of their online session) while others may utilize numerous proxies.
One solution you can implement to alleviate the static IP issue is to take a substring of the IP address and use the the first 3/4 of an IPv4 address (24 bits / 3 bytes / third octet). If validation fails utilizing this method logout will be enforced and the user will be required to log back in, thereby updating their IP address. Please note that this method may be bypassed by IP spoofing.
I have already posted an article on retrieving a client’s ip so I will not go over that here. Instead, I will simply demonstrate an easy method for dropping the fourth octet.
1234567891011121314151617
<?php
// sample IP
$ip = '192.168.1.100';
/**
* Trims the IP address and returns it in the
* format XXX.XXX.XXX.0
*/
function trimIP($ip) {
$pos = strrpos($ip, '.');
if ($pos !== false) {
$ip = substr($ip, 0, $pos+1);
}
return $ip . '.0';
}
$ip = trimIP($ip);
view rawclass-c-ip.php hosted with ❤ by GitHub
To enhance session security utilizing IP address checking, follow these steps:
  1. Obtain the user’s IP address and run it through the trimIP() function on page load.
  2. Check the IP address against a session variable, i.e. $_SESSION['user_ip'], to see if they match.
  3. If the new IP address does not match the session IP and the session IP address is not empty, invalidate the current logged in user’s session.
For the sake of not leaving anything out, here’s a proof of concept for validating IP addresses:
123456789101112131415161718192021
<?php
// assumes you have set the session variable logged_in to a boolean value depending on login status
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] == false) {
// get_ip_address() can be found on another post referenced in this article
$_SESSION['ip_address'] = get_ip_address();
} else {
if ($_SESSION['ip_address'] !== get_ip_address()) {
// destroy
session_destroy();
$_SESSION = array();
if (!headers_sent()) {
// set a flash and redirect to the login page
header('Status: 200');
header('Location: ' . urlencode('/login'));
exit;
} else {
// throw an error message
exit;
}
}
}

User Agent Validation

Many individuals prefer validating the user’s browser agent as opposed to their IP address because the information should remain static over the duration of the site visit. There exist a plethora of different user agent strings depending on both your browser, operating system, and versioning. An example user agent would be:
Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.10) Gecko/2009042316 Firefox/3.0.10 (.NET CLR 3.5.30729)
Since user agents are browser dependent and a user’s session is only valid within their currently opened browser, validating the user agent is a great way to enhance security of your site. Below is a quick proof of concept implementation on how to go about adding a user agent validation check to your existing session handling:
123456789101112131415161718192021
<?php
// assumes you have set the session variable logged_in to a boolean value depending on login status
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] == false) {
$_SESSION['user_agent'] = (isset($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : '';
} else {
// if the user agent doesnt validate, destroy the session and force relogin
if (!isset($_SERVER['HTTP_USER_AGENT']) || $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
// destroy
session_destroy();
$_SESSION = array();
if (!headers_sent()) {
// set a flash and redirect to the login page
header('Status: 200');
header('Location: ' . urlencode('/login'));
exit;
} else {
// throw an error message
exit;
}
}
}
As a wrap up, I would ultimately recommend incorporating both user agent validation as well as IP address validation into my session/authentication handling.

Comments

Popular Posts