Is the following class well-built and secure or am I missing something? Any improvement ideas welcome.
/*
* This class is for generating and using one single save session.
*/
final class Session {
private static $name;
private static $force_ssl_cookie;
private static $destroy_unset;
private static $max_inactivity;
private static function checkInit() {
if(!isset(self::$name)) {
throw new Exception("Cannot use Session class without calling setSessionName once first.");
}
if(!isset(self::$force_ssl_cookie)) {
throw new Exception("Cannot use Session class without calling setForceSSLCookie once first.");
}
if(!isset(self::$destroy_unset)) {
throw new Exception("Cannot use Session class without calling setSessionDestroyUnset once first.");
}
if(!isset(self::$max_inactivity)) {
throw new Exception("Cannot use Session class without calling setMaxInactivity once first.");
}
}
public static function setName($name) {
if(isset(self::$name)) {
throw new Exception("Session class misuse: A session name has already been set.");
}
self::$name = $name;
}
public static function getName() {
return self::$name;
}
public static function setForceSSLCookie($force_ssl_cookie) {
if(isset(self::$force_ssl_cookie)) {
throw new Exception("Session class misuse: The parameter force_ssl_cookie has already been set.");
}
self::$force_ssl_cookie = $force_ssl_cookie;
}
public static function setDestroyUnset($destroy_unset) {
if(!is_array($destroy_unset)) {
throw new Exception("Session class misuse: The parameter session_destroy_unset must be an array.");
}
if(isset(self::$destroy_unset)) {
throw new Exception("Session class misuse: The parameter session_destroy_unset has already been set.");
}
self::$destroy_unset = $destroy_unset;
}
public static function setMaxInactivity($max_inactivity) {
if(isset(self::$max_inactivity)) {
throw new Exception("Session class misuse: The parameter max_inactivity has already been set.");
}
self::$max_inactivity = $max_inactivity;
}
/*
* Destroy the current session and start a new one.
*/
public static function restart() {
self::destroyAbsolute();
self::start();
}
/**
* Secure start of a session.
*
* @return type
*/
public static function start() {
self::checkInit();
session_name(self::$name);
session_set_cookie_params(0, COOKIEPATH, null, self::$force_ssl_cookie, true);
$fresh = false;
// no session existing, make a new session
if(!isset($_COOKIE[self::$name])) {
// replace the Session ID
session_id(self::getSaveSessionId());
$fresh = true;
}
session_start();
// If the session was already existing, client_ip must match, the session must still be active etc.
if (!$fresh) {
// If this client ip does not match the ip of the current client, this might be an attack --> destroy session
if (self::getClientIP() === $_SESSION["client_ip"] && self::getClientAgent() === $_SESSION['client_agent']) {
if(self::checkStillActive()) {
return true;
}
} else {
self::destroyAbsolute();
header('Location: '.$_SERVER['REQUEST_URI']);
die;
}
} else {
// if we have to create a new session, we do it in a secure, self-defined way
self::setClientIP();
self::setClientAgent();
$_SESSION['last_activity'] = time();
return true;
}
}
// creates a new, secure session id (MUST be called BEFORE session_start())
public static function getSaveSessionId() {
self::checkInit();
$hash_time = md5(microtime());
$hash_ip = md5($_SERVER["REMOTE_ADDR"]);
$hash_space = sha1(disk_free_space(getcwd()));
return sha1($hash_time . $hash_ip . $hash_space);
}
/**
* Secure instant destruction of session. Must be called after session_start !
*/
public static function destroyAbsolute() {
self::checkInit();
session_name(self::$name);
session_set_cookie_params(0, COOKIEPATH, null, self::$force_ssl_cookie, true);
if(session_id()) {
if (isset($_COOKIE[session_name()])) {
setcookie(session_name(), "", time() - 42000, COOKIEPATH);
}
unset($_COOKIE[session_name()]);
session_destroy();
}
}
// retrieve the current session id
public static function getCurrentId() {
self::checkInit();
session_name(self::$name);
return session_id();
}
/**
* Destroys a session with a specific ID.
*
* @param type $sessid: Session ID.
*/
public static function destroySpecific($sessid) {
if($sessid && !empty($sessid)) {
session_name(self::$name);
session_id($sessid);
// session_start();
foreach(self::$destroy_unset as $uns) {
unset($_SESSION[$uns]);
}
session_destroy();
}
}
public static function checkStillActive() {
self::checkInit();
// make sure that a user was not inactive for too long
if(intval($_SESSION['last_activity']) < time()-self::$max_inactivity) { //have we expired?
self::destroyAbsolute();
header('Location: '.$_SERVER['REQUEST_URI']);
die;
} else { //if we haven't expired:
$_SESSION['last_activity'] = time(); //this was the moment of last activity.
return true;
}
}
// sets the session IP of the current client
private static function setClientIP() {
if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$_SESSION["client_ip"] = md5($_SERVER["HTTP_X_FORWARDED_FOR"]);
} else {
$_SESSION["client_ip"] = md5($_SERVER["REMOTE_ADDR"]);
}
}
// retrieves the client IP of the session owner
private static function getClientIP() {
if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) {
return md5($_SERVER["HTTP_X_FORWARDED_FOR"]);
} else {
return md5($_SERVER["REMOTE_ADDR"]);
}
}
private static function setClientAgent() {
$_SESSION['client_agent'] = md5($_SERVER['HTTP_USER_AGENT']);
}
private static function getClientAgent() {
return md5($_SERVER['HTTP_USER_AGENT']);
}
}
Example of usage:
Session::setName("SESSION-mysession");
Session::setForceSSLCookie($force_ssl_cookie);
Session::setMaxInactivity(PHP_INT_MAX); // no problem so far
Session::setDestroyUnset(array('example'));
// create a new session, if no session is initialized
Session::start();