Web application authentication
Because there are so many attack vectors in regards to web based applications researching some of the more common attacks takes time but may very well save you valuable programming time if you are 'rolling your own' authentication routine.
Some thing's you may or may not be aware of are attack vectors such as session fixation, session poisoning, session hijacking, cross site request forgeries, replay, cross site scripting, SQL injection etc. etc. etc.
As you can imagine web authentication and web application programming may seem somewhat daunting to say the least. Doing your research, testing your code and even utilizing free software available on the web can help you create a secure authenticated web application.
Validate your input
Validating your input forms is probably the most important piece of advice I can give you. Whether it is a URL based variable passed to your application like the following example,
http://www.your-web-site.com/?var=myId
Or even a contact form that accepts post data you must validate your input or your web application will be vulnerable to any of the methods I mentioned above. If you wish to read about validating input in detail please read my earlier post 'web application security'
Another piece of information you might want to research would be the utilization of encryption. Encryption is a method of hiding data and can be a very worthwhile feature of your application.
Authenticating your users
When it comes time to authenticate your users I want to provide a simple checklist of items to think of before you begin writing your authentication routine:
- Authentication form
- Validating input before transmission
- Encrypting user input before transmission
- Primary authentication function
- Validating input prior to user lookup
- Generating & validating SQL statement
- True or false result of user lookup
- Generating a private/public encryption key pair
- Generate a series of variables based on unique visitor to be used during secondary authentication functions
- Generate an authentication token to be used as persistent session data
- Validating server response to user lookup
- Secondary or RE authentication function
- Validating input prior to user lookup
- Generating & validating SQL statement
- True or false result of user lookup
- Decoding of authentication token
- Ensure decoded variables match encrypted saved database information
At this point you should have enough to write a web application layer authentication routine to protect your pages from un-authorized access. I say 'web application layer' because as long as there are no underlying vulnerabilities within the web server or scripting language your authentication application should be secure.
Encryption class
class encrypt { private $data; function ReadFolder( $folder ) { if( empty( $folder ) ) { $this->data = -1; } else { $rand_image = array(); if( $handle = opendir( $folder ) ) { while( false !== ( $file = readdir( $handle ) ) ) { if( ( $file != "." ) && ( $file != ".." ) && ( $file != "index.html" ) && ( !is_dir( $file ) ) ) { $this->value[] = $file; } } closedir( $handle ); } } return $this->value; } function MakeSuperRandom() { return srand( ( double ) microtime( time() ) * 100000 ); } function PickRandomImages( $array ) { $num1 = count( $array ); $num1 = $num1 - 1; $this->MakeSuperRandom(); $img_num = rand( 3, $num1 ); $this->data[] = $array[$img_num]; $num2 = count( $array ); $num2 = $num2 - 1; $this->MakeSuperRandom(); $img_num = rand( 3, $num2 ); $this->data[] = $array[$img_num]; $num3 = count( $array ); $num3 = $num3 - 1; $this->MakeSuperRandom(); $img_num = rand( 3, $num3 ); $this->data[] = $array[$img_num]; return $this->data; } function GeneratePrivKey( $array ) { if( empty( $array ) ) { $this->data = -1; } else { for( $x = 0; $x < count( $array ); $x++ ) { if( function_exists( "mhash" ) ) { $keys[] = mhash( MHASH_SHA1, sha1( $array[$x] ) ); } elseif( function_exists( sha1 ) ) { $keys[] = sha1( sha1( $array[$x] ) ); } else { $keys[] = md5( md5( $array[$x] ) ); } } for( $y = 0; $y < count( $keys ); $y++ ) { if( count( $keys ) == $keys[$y] ) { $this->data .= $keys[$y]; } else { $this->data .= $keys[$y] . ":"; } } } return $this->data; } function GeneratePublicKey( $data ) { return md5( $data ); } function GeneratePrivateKey( $a ) { $x = $this->ReadFolder( $a ); $y = $this->PickRandomImages( $x ); $this->data = $this->GeneratePrivKey( $y ); return $this->data; } function EncodePrivToHex( $key ) { return bin2hex( $key ); } function DecodePrivToBin( $key ) { $hexLenght = strlen( $key ); if( $hexLenght % 2 != 0 || preg_match( "/[^\da-fA-F]/", $key ) ) { $binString = -1; } unset( $binString ); for( $x = 1; $x <= $hexLenght/2; $x++ ) { $this->data .= chr( hexdec( substr( $key, 2 * $x - 2, 2 ) ) ); } return $this->data; } function EncodeAuthTokenHeavy( $array, $iv, $time, $public, $server ) { if( function_exists( "mcrypt_encrypt" ) ) { $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB); // encrypt the username, password, level & group attributes (as base64_encoded values) $uname = base64_encode( $cipher->encrypt( $array[0]['username'], $public ) ); $passwd = base64_encode( $cipher->encrypt( $array[0]['password'], $public ) ); $alevel = base64_encode( $cipher->encrypt( $array[0]['level'], $public ) ); $agroup = base64_encode( $cipher->encrypt( $array[0]['group'], $public ) ); } else { $uname = base64_encode( $array[0]['username'] ); $passwd = base64_encode( $array[0]['password'] ); $alevel = base64_encode( $array[0]['level'] ); $agroup = base64_encode( $array[0]['group'] ); } return $uname . "::" . $passwd . "::" . $alevel . "::" . $agroup . "::" . $time . "::" . md5( $server['REMOTE_ADDR'] ) . "::" . base64_encode( $_SERVER['HTTP_REFERER'] ) . "::" . md5( $server['HTTP_USER_AGENT'] ) . "::" . md5( $iv ) . "::" . $public; } function DecodeAuthTokenHeavy( $token ) { $array = preg_split( "/::/", $token ); if( function_exists( "mcrypt_encrypt" ) ) { $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB); $array[0] = $cipher->decrypt( base64_decode( $array[0] ), $array[9], $array[8] ); $array[1] = $cipher->decrypt( base64_decode( $array[1] ), $array[9], $array[8] ); $array[2] = $cipher->decrypt( base64_decode( $array[2] ), $array[9], $array[8] ); $array[3] = $cipher->decrypt( base64_decode( $array[3] ), $array[9], $array[8] ); } else { $array[0] = base64_decode( $array[0] ); $array[1] = base64_decode( $array[1] ); $array[2] = base64_decode( $array[2] ); $array[3] = base64_decode( $array[3] ); } return $array; } } /** * Cipher * * Simple mcrypt interface. * * Cipher is a simple class for working with mcrypt. * * @package Cipher * @author Nathan Lucas* @link http://www.gimpstraw.com/ * @copyright Copyright (c) 2008, Nathan Lucas * @version 2.0.0 * * Added $iv to both encrypt() and decrypt() allowing you to use preset IVs * while encrypting/decrypting data. * * Also added getIV(), which returns the instance's current IV in base64 * allowing you to store this IV for use on other instances of Cipher. */ class Cipher { /** * Algorithm to use. * * @access private * @var string */ private $algo; /** * Encryption mode. * * @access private * @var string */ private $mode; /** * Randomization source. * * @access private * @var integer */ private $source; /** * Initialization vector. * * @access private * @var string */ private $iv = null; /** * Encryption key. * * @access private * @var string */ private $key = null; /** * Cipher($algo, $mode, $source) * * Cipher constructor. Sets the algorithm being used, the encryption * mode, and the IV. * * @param string $algo * @param string $mode * @param integer $source (randomization source) * @access public * @return void */ public function __construct($algo = MCRYPT_3DES, $mode = MCRYPT_MODE_CBC, $source = MCRYPT_RAND) { $this->algo = $algo; $this->mode = $mode; $this->source = $source; if (is_null($this->algo) || (strlen($this->algo) == 0)) { $this->algo = MCRYPT_3DES; } if (is_null($this->mode) || (strlen($this->mode) == 0)) { $this->mode = MCRYPT_MODE_CBC; } } /** * encrypt($data, $key, $iv) * * Returns encrpyted $data, base64 encoded. $key must be specified at * least once, it can be changed at any point. * * @param string $data * @param mixed $key * @param string $iv * @access public * @return string */ public function encrypt($data, $key = null, $iv = null) { $key = (strlen($key) == 0) ? $key = null : $key; $this->setKey($key); $this->setIV($iv); $out = mcrypt_encrypt($this->algo, $this->key, $data, $this->mode, $this->iv); return base64_encode($out); } /** * decrypt($data, $key, $iv) * * Returns decrypted $data. $key must be specified at least once, it can * be changed at any point. * * @param mixed $data * @param mixed $key * @param string $iv * @access public * @return string */ public function decrypt($data, $key = null, $iv = null) { $key = (strlen($key) == 0) ? $key = null : $key; $this->setKey($key); $this->setIV($iv); $data = base64_decode($data); $out = mcrypt_decrypt($this->algo, $this->key, $data, $this->mode, $this->iv); return trim($out); } /** * getIV() * * Returns the IV used for encryption so you can use it again in another * Cipher instance to decrypt data. * * @access public * @return string */ public function getIV() { return base64_encode($this->iv); } /** * setIV($iv) * * Sets IV. If $iv is specified, the instance IV will be set to this. If not, * the instance will generate an IV. * * @param string $iv * @access private * @return void */ public function setIV($iv) { if (!is_null($iv)) { $this->iv = base64_decode($iv); } if (is_null($this->iv)) { $iv_size = mcrypt_get_iv_size($this->algo, $this->mode); $this->iv = mcrypt_create_iv($iv_size, $this->source); } } /** * setKey($data, $key) * * Sets Cipher::key. This will be the key used for the encrypt and decrypt * methods until another $key is specified. This will trigger an error if * no initial key is set. * * @param mixed $key * @access private * @return void */ private function setKey($key) { if (!is_null($key)) { $key_size = mcrypt_get_key_size($this->algo, $this->mode); $this->key = hash("sha256", $key, true); $this->key = substr($this->key, 0, $key_size); } if (is_null($this->key)) { trigger_error("You must specify a key at least once in either Cipher::encrpyt() or Cipher::decrypt().", E_USER_ERROR); } } }
Authentication class
class authenticate { function DecideAuth( $token, $user, $pass, $server ) { if( isset( $token ) ) { return $this->ReAuthenticate( $token ); } else { return $this->PrimaryAuthentication( $user, $pass, $server ); } } function checkLDAP( $user, $pass ) { global $defined; global $handles; // are we configured to use ldap? if( ( !empty( $defined['ldapuser'] ) ) && ( !empty( $defined['ldappass'] ) ) && ( !empty( $defined['ldapdomain'] ) ) && ( !empty( $defined['ldapserv'] ) ) && ( !empty( $defined['ldapport'] ) ) && ( !empty( $defined['binddn'] ) ) && ( !empty( $defined['basedn'] ) ) ) { // attempt to bind for this user as long as there is at least a username // found in the local mysql database $data = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ); $query = "SELECT * FROM `users` WHERE `username` = \"" . $user . "\""; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) !== -1 ) { if( $handles['db']->dbNumRows( $value ) > 0 ) { // proceed to bind because a user was found locally //echo "AUTH:
"; var_dump( $handles['ldap']->auth( $user, $pass, $defined['ldapserv'], $defined['ldapport'] ) ); if( $handles['ldap']->auth( $user, $pass, $defined['ldapserv'], $defined['ldapport'] ) === 0 ) { // a good bind? lets propogate the user/pass to the database to limit ldap queries /cached $sql = "UPDATE `users` SET `password` = sha1( \"" . $pass . "\" ) WHERE `username` = \"" . $user . "\" LIMIT 1"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $data ), $data ) ) !== -1 ) { if( $handles['db']->dbNumRowsAffected( $data ) === 1 ) { $ret = 0; } else { $ret = 1; } } else { $ret = 2; } } else { $ret = 3; } } else { $ret = 4; } } else { $ret = 5; } } else { $ret = 6; } $handles['ldap']->closeConn(); return $ret; } function PrimaryAuthentication( $user, $pass, $server ) { global $defined; global $handles; // ensure our data is present if( ( empty( $user ) ) || ( empty( $pass ) ) || ( count( $server ) < 0 ) ) { $ret = 1; } else { // validate user credentials if( ( $handles['val']->ValidateAlphaChar( $user ) === -1 ) || ( $handles['val']->ValidatePassword( $pass ) === -1 ) ) { $ret = 2; } else { // ok now we need to look the user up $data = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ); $query = "SELECT * FROM `users` WHERE `username` = \"" . $user . "\" AND `password` = sha1( \"$pass\" )"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) === -1 ) { $ret = 3; } else { if( $handles['db']->dbNumRows( $value ) <= 0 ) { // check ldap real quick... if( $this->checkLDAP( $user, $pass ) === 0 ) { $ret = 0; } else { $ret = 4; } } else { $array = $handles['db']->dbArrayResultsAssoc( $value ); $array[0]['password'] = $pass; // reassign $array[0]['password'] hash to submitted password // since we have a valid user we need to perform the following: // + Create a private, and public encryption key pair // + Update the user record to store the private key // + Generate a re-usable encrypted token we can pass // back and forth between applications $privatekey = $handles['encrypt']->EncodePrivToHex( $handles['encrypt']->GeneratePrivateKey( $defined['enckeygen'] ) ); $publickey = $handles['encrypt']->GeneratePublicKey( $privatekey ); // since we are going to store the session private key iv data in the database we // must initialize the encryption class (if the mcrypt libs are present) if( function_exists( "mcrypt_encrypt" ) ) { $cipher = new Cipher(MCRYPT_BLOWFISH, MCRYPT_MODE_ECB); $cipher->setIV(); $sessioniv = $cipher->getIV(); } else { // since this php configuration does not // utilize the mcrypt libs we need to generate // a spoofed iv $sessioniv = rand( microtime(), microtime() ); } // create some time stamp data $access_date = $handles['misc']->GenDate(); $access_time = $handles['misc']->GenTimeRead(); $time = $handles['misc']->GenTime(); // destroy any existing sessions for this user $query = "UPDATE `users` SET `ip` = '', `referrer` = '', `agent` = '', `session` = '', `iv` = '' WHERE `id` = \"" . $array[0]['id'] . "\" LIMIT 1"; $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ); // update user record with session requirements $query = "UPDATE `users` SET `ip` = \"" . md5($server['REMOTE_ADDR']) . "\", `referrer` = \"" . $server['HTTP_REFERER'] . "\", `agent` = \"" . md5($server['HTTP_USER_AGENT']) . "\", `access_date` = \"" . $access_date . "\", `access_time` = \"" . $access_time . "\", `session` = \"" . $privatekey . "\", `iv` = \"" . $sessioniv . "\" WHERE `id` = \"" . $array[0]['id'] . "\""; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $query, $data ), $data ) ) === -1 ) { $ret = 5; } else { if( ( $token = $handles['encrypt']->EncodeAuthTokenHeavy( $array, $sessioniv, $time, $publickey, $server ) ) === -1 ) { $ret = 6; } else { $ret = 0; } } } } } } if( $ret === 0 ) { if( eregi( $defined['hostname'], $server['HTTP_REFERER'] ) ) { $handles['session']->regen( true ); } $_SESSION['token'] = $token; } $handles['db']->dbFixTable( "sessions", $dbconn ); $handles['db']->dbFixTable( "users", $dbconn ); return $ret; } /* * During the PrimaryAuthentication function we created a private and public key * The private key gets placed in the users record for later use * * The private key is passed to the encode function and is used to encrypt and base64_encode * the various user attributes we require during the Re-Authentication function. * * The process takes the following steps during re-authentication * 1. Ensure we have a registered and valid session token * 2. Split the token apart and use the base64_decode function to decode each element * 3. Use the last element of the session token (the public key) to unencrypt each element * 4. Pass the array of unencrypted elements to check the following: * + Ensure the remote IP address is the same as the one registered during the primary authentication * + Ensure the referring page is coming from something located on the $defined['server'] ie. http://server.com/page.php is from server.com * + Ensure the browser agent string has not changed as well * + Make sure the user has not been timed out based on the authenticated time stamp vs. the defined timeout variable * + Do a lookup on the decoded username and password to ensure they are indeed a valid user in the database * + Regenerate the session id, destroy everything and let the user continue about their business */ function ReAuthenticate( $token ) { global $defined; global $handles; // this next step will give us plaintext of the base64_encoded / encrypted token elements if( isset( $token ) ) { if( ( $array = $handles['encrypt']->DecodeAuthTokenHeavy( $token ) ) === 1 ) { return 7; } } if( count( $array ) < 10 ) { $ret = 8; } else { // do a lookup based on the username $sql = "SELECT * FROM `users` WHERE `username` = \"" . $array[0] . "\" LIMIT 1"; $dbconn = $handles['db']->dbConnect( $defined['dbhost'], $defined['username'], $defined['password'], $defined['dbname'] ); if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $dbconn ), $dbconn ) ) === -1 ) { $ret = 9; } else { if( count( $handles['db']->dbNumRows( $value ) ) <= 0 ) { $ret = 10; } else { $user_data = $handles['db']->dbArrayResultsAssoc( $value ); if( ( md5( $user_data[0]['session'] ) !== $array[9] ) || ( md5( $user_data[0]['iv'] ) !== $array[8] ) ) { $ret = 11; } else { // no check ip, referrer and browser agent if( ( $user_data[0]['ip'] !== $array[5] ) || ( !eregi( $defined['hostname'], $user_data[0]['referrer'] ) ) || ( $user_data[0]['agent'] !== $array[7] ) ) { $ret = 12; } else { // make sure they haven't expired their session if( ( $this->AuthTimeOut( $defined['timeout'], $array[4], $handles['misc']->GenTime() ) ) === -1 ) { $ret = 13; } else { // all right, make sure the username, password within decrypted token info is // valid and let the user proceed $sql = "SELECT * FROM `users` WHERE `username` = \"" . $array[0] . "\" AND `password` = \"" . sha1( $array[1] ) . "\" LIMIT 1"; if( ( $value = $handles['db']->dbQuery( $handles['val']->ValidateSQL( $sql, $dbconn ), $dbconn ) ) === -1 ) { $ret = 14; } else { if( count( $handles['db']->dbNumRows( $value ) ) !== 1 ) { $ret = 15; } else { $handles['session']->regen( true ); return 0; } } } } } } } } return $ret; } function AuthTimeOut( $constant, $time, $current ) { if( ( $current - $time ) > $constant ) { $data->value = -1; } else { $data->value = 0; } return $data->value; } }
Database structure for user accounts (users, groups, access levels)
CREATE TABLE `users` ( `id` int(255) NOT NULL auto_increment, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `level` varchar(8) NOT NULL, `group` varchar(40) NOT NULL, `dept` varchar(40) NOT NULL, `first` varchar(255) NOT NULL, `last` varchar(255) NOT NULL, `phone` varchar(12) NOT NULL, `email` varchar(255) NOT NULL, `ip` blob, `referrer` blob, `agent` blob, `create_date` varchar(255) NOT NULL, `create_time` varchar(255) NOT NULL, `access_date` varchar(255) NOT NULL, `access_time` varchar(255) NOT NULL, `session` blob, `iv` blob, `reset` varchar(8) NOT NULL, `owner` varchar(45) NOT NULL, UNIQUE KEY `username` (`username`), KEY `id` (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
CREATE TABLE `groups` ( `description` varchar(255) NOT NULL, `manager` varchar(80) NOT NULL, `contact` varchar(80) NOT NULL, `id` int(255) NOT NULL auto_increment, `group` varchar(85) NOT NULL, `owner` varchar(45) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `group` (`group`) ) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
CREATE TABLE `accesslevel` ( `id` int(255) NOT NULL auto_increment, `level` varchar(40) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;
0 comments:
Post a Comment