v2 moved to php5 and made the library as a class v2 -> v2.1 rewrote how verification token is stored, took out the ip checks v2.1. -> 2.2 some editorial changes to comments, added $is_use_include_path flast to the serverkeyfile constructor, added regenerate timeout (a period of time after which an automatice regenerate id is performed ) LICENSE: This code can be used, modified or redistributed with the following restriction: The original copyright notice and url link must remain where it is. DOCS: 1. First the rational to use this library SecureSession class extends the functionality of the built in php session mechanism. It does not replace it but only enhances it. The plain use of session_start() is fine as long as the programmer is aware that session hijacking is not very difficult for a clever hacker. A hacker can for example inject some malicious javascript into a website in order to steal the users session id. Another type of attack that this class helps wiht is session fixation. Key to preventing that type of attack is to use SecureSession::regenerate_id() as soon as privilage level in the application has changed. For example right after the users password is verified. Summary of the features: a. Tie session id to the user browser's user agent string b. Let only session ids generated by the server be valid c. Provide replacement functions for session_start(), session_regenerate_id() d. Provide security exception in case an invalid session is detected 2. API documentation Public static method SecureSession::start( ); This is pretty much a replacement method for session_start() This method may throw InvalidSession exception in case an invalid session id is detected. Not every invalid session id is the result of a hacking attemp so a redirect to login page with logging is usually more appropriate action. Public static method SecureSession::regenerate_id( ); This method is a wrapper around session_regenerate_id() and should work pretty much the same except the extra security is maintained. Public static method SecureSession::setServerKey( ServerKey $server_key ); This method sets the server key to use in order to tie every session to the specific server cryptographicaly. Without knowing the secret server key an attacker will not be able to generate a valid session id on their own. The library comes with two simple subclasses of ServerKey that can be used without writing your own. They are ServerKeySimple and ServerKeyFile. ServerKeySimple simply takes the key as an argument in its constructor. ServerKeyFile takes a filename and all the contents of the file are used as a key. Note that a plain exception is thrown in case the key file cannot be read. You can write your own ServerKey by subclassing ServerKey and implementing getKey() method. For example a much more secure method would be to run some kind of setuid process that would be able to read a key with limited permissions. Public static method SecureSession::setKeyFlags( $key_flags ) The argument $key_flags is a bitmask of a number of possible constants: SecureSession::CHECK_USER_AGENT SecureSession::CHECK_SERVER_KEY SecureSession::CHECK_ALL Since CHECK_ALL is the default behaviour of the class there is no need to call this method but its provided for the purpose of flexibility. Remember that dropping any of the two checks will only decrease the strength of the overall security method. EXAMPLE USAGE: SecureSession::setServerKey( new ServerKeyFile( "./key.txt" ) ); SecureSession::setKeyFlags( SecureSession::CHECK_ALL ); // not really needed try { SecureSession::start( ); } catch ( InvalidSession $e ) { // log the occurance first ... header( "Location: login.php" ); } 3. Requirments a. Mhash extension needs to be installed 4. Notes: a. A check is made against session.use_only_cookies configuration setting. If url based sid is not allowed and client passed us one it will be ignored. b. If an sid is received that cannot be verified and php session.use_cookies is 1 an attempt is made to unset the sid of the caller. This may break a session fixation attack attempt. c. php also supports passing sid as part of url path this library does not handle that method so if anyone has a problem with that let me know. */ class InvalidSession extends Exception {}; abstract class ServerKey { protected $key = null; public function getKey( ) { return $this->key; } } class ServerKeySimple extends ServerKey { public function __construct( $key ) { $this->key = $key; } } class ServerKeyFile extends ServerKey { public function __construct( $keyfile, $use_include_path = false ) { $this->key = file_get_contents( $keyfile, $use_include_path ); if( $this->key === FALSE ) { throw new Exception( "Could not read keyfile" ); } } } class SecureSession { const CHECK_USER_AGENT = 1; const CHECK_SERVER_KEY = 4; const CHECK_ALL = 5; static protected $server_key = null; static protected $key_flags = self::CHECK_ALL; static public function setServerKey( ServerKey $server_key ) { self::$server_key = $server_key->getKey( ); } static public function setKeyFlags( $key_flags ) { self::$key_flags = $key_flags; } static public function start( ) { if(! extension_loaded( "mhash" ) ) { throw new Exception( "mhash extension is required to use start()" ); } // If a session was not started then we start it if(! $cur_id = self::get_sess_id( ) ) { // first we need to star the session and let php // generate a random session id and set the session cookie session_start( ); $verify_token = self::generate_verify_token( session_id( ) ); self::set_verify_token( $verify_token ); return; } // make sure the verify token is actually present if(! isset( $_COOKIE[ session_name( ) . '_verify' ] ) ) { self::security_exception(); } $mac_remote = $_COOKIE[ session_name( ) . '_verify' ]; $key = self::get_key( ); // verify that the mac is valid if( $mac_remote == bin2hex( mhash( MHASH_SHA1, $cur_id , $key ) ) ) { session_start( ); } else { // failed to verify self::security_exception(); } } static public function regenerate_id( ) { session_regenerate_id( ); // reset the verify cookie with the new info $verify_token = self::generate_verify_token( session_id( ) ); self::set_verify_token( $verify_token ); } // A more thoughrow way to get rid of a session static public function kill( ) { if( isset( $_COOKIE[ session_name( ) ] ) ) { setcookie( session_name( ), '', 0, '/' ); } if( isset( $_COOKIE[ session_name( ) . '_verify' ] ) ) { setcookie( session_name( ), '', 0, '/' ); } session_destroy( ); } //////////////////////////////////////////// // // BELOW are internal functions to the class // //////////////////////////////////////////// static protected function generate_verify_token( $cur_session_id ) { $key = self::get_key( ); // sign the code with the session key $verify_token = bin2hex( mhash( MHASH_SHA1, $cur_session_id , $key ) ); return( $verify_token ); } static protected function set_verify_token( $verify_token ) { // get the cookie parameters for the session $params = session_get_cookie_params( ); // set the verification cookie and use the params of // the current session. The name of the cookie will be // the name of the session cookie with _verify appended setcookie( session_name( ) . '_verify' , $verify_token , $params['lifetime'], $params['path'], $params['domain'], $params['secure'] ); } static protected function security_exception( ) { if( ini_get( "session.use_cookies" ) ) { // try to unset cookie and verify token $params = session_get_cookie_params( ); setcookie( session_name( ) , "" , $params['lifetime'], $params['path'], $params['domain'], $params['secure'] ); setcookie( session_name( ) . '_verify' , "" , $params['lifetime'], $params['path'], $params['domain'], $params['secure'] ); } throw new InvalidSession( "Invalid Session detected" ); } static protected function get_sess_id( ) { $sess_name = session_name( ); if( isset( $_COOKIE[$sess_name] ) ) { $cur_id = $_COOKIE[$sess_name]; } else if( isset( $_GET[$sess_name] ) ) { if(! ini_get( "session.use_only_cookies" ) ) { $cur_id = $_GET[$sess_name]; } else { // if they sent an sid cookie in url just ignore it $cur_id = false; } } else { $cur_id = false; } return( $cur_id ); } static protected function get_key( ) { if( ( self::$key_flags & self::CHECK_USER_AGENT ) && isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $uagent = $_SERVER['HTTP_USER_AGENT']; } else { $uagent = 'no-agent'; } if( self::$key_flags & self::CHECK_SERVER_KEY ) { if( isset( self::$server_key ) ) { $server_key = self::$server_key; } else { trigger_error( "No server key set, you may want to use SecureSession::setServerKey() to set the key", E_USER_WARNING ); $server_key = 'no-server-key'; } } else { $server_key = 'no-server-key'; } $key = $uagent . $server_key; return( $key ); } } // class ?>