RSS 2.0 Feed
Posted on November 20th, 2009 at 06:57 PM by Corey Ballou

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.

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 (MD5, SHA1, SHA256, Whirpool, 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

CREATE 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;

Part 2: The One-Way Password Hashing Algorithm

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

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

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;

Part 2: The pseudo-random salt generator

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

Part 3: The One-Way Password Hashing Algorithm

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

/**
 * @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

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;

Part 2: The One-Way Password Hashing Algorithm

/* 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);
}

Part 3: The Password Validation Function

/**
 * @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));
}

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.

// 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);

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:

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:

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 handler.

Related Posts

Leave a Reply

Allowable tags
a, abbr, b, blockquote, cite, code, em, i, strike, strong, pre lang, line

* comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.