Chapter 6 - Cookies and sessions
When we created CSRF helper in the last chapter we briefly mentioned that we'll need to tighten the security of our sessions. So now we will do just that. First we need to define three new variables in our config.php file
define('SESSION_PATH', ROOT_PATH . 'application/storage/sessions/');
define('SESSION_KEY', 'randomstringofcharacters');
define('SESSION_TTL', 60);
Now create a new file named "SecureSession.php" inside the /application/helpers/
folder.
<?php
namespace application\helpers;
use SessionHandler;
class SecureSession extends SessionHandler
{
protected $key, $name, $cookie;
public function __construct($key = SESSION_KEY, $name = 'SESSION')
{
$this->key = $key;
$this->name = $name;
if(!isset($_SESSION)) {
$this->setup();
}
}
protected function setup()
{
ini_set('session.use_cookies', 1);
ini_set('session.use_only_cookies', 1);
session_name($this->name);
}
}
It's also a good idea to have a session encrypted so lets also add that
private function _encrypt($data, $key) {
$encryption_key = base64_decode($key);
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
if($iv===false) die('Fatal error: encryption was not successful - could not save data!!'); // weak encryption
$encrypted = openssl_encrypt($data, 'aes-256-cbc', $encryption_key, 0, $iv);
return base64_encode($encrypted . '::' . $iv);
}
private function _decrypt($data, $key) {
$encryption_key = base64_decode($key);
list($encrypted_data, $iv) = explode('::', base64_decode($data), 2);
return openssl_decrypt($encrypted_data, 'aes-256-cbc', $encryption_key, 0, $iv);
}
//and abstract it
public function _read($id)
{
return $this->_decrypt(parent::read($id), $this->key);
}
public function _write($id, $data)
{
return parent::write($id, $this->_encrypt($data, $this->key));
}
Now we can add the ussual session functionality
public function start()
{
if (session_id() === '') {
session_start();
return (mt_rand(0, 4) === 0) ? $this->refresh() : true; // 1/5
}
return false;
}
public function forget()
{
if (session_id() === '') {
return false;
}
$_SESSION = [];
return session_destroy();
}
public function refresh()
{
return session_regenerate_id(true);
}
We should also have some method for validating sessions
public function isExpired($ttl = SESSION_TTL)
{
$activity = isset($_SESSION['_last_activity'])
? $_SESSION['_last_activity']
: false;
if ($activity !== false && time() - $activity > $ttl * 60) {
return true;
}
$_SESSION['_last_activity'] = time();
return false;
}
public function isFingerprint()
{
$hash = md5(
$_SERVER['HTTP_USER_AGENT'] .
(ip2long($_SERVER['REMOTE_ADDR']) & ip2long('255.255.0.0'))
);
if (isset($_SESSION['_fingerprint'])) {
return $_SESSION['_fingerprint'] === $hash;
}
$_SESSION['_fingerprint'] = $hash;
return true;
}
//and abstarct it
public function isValid($ttl = SESSION_TTL)
{
return ! $this->isExpired($ttl) && $this->isFingerprint();
}
And final two methods are for getting and setting values from the session
public function get($name)
{
$parsed = explode('.', $name);
$result = $_SESSION;
while ($parsed) {
$next = array_shift($parsed);
if (isset($result[$next])) {
$result = $result[$next];
} else {
return null;
}
}
return $result;
}
public function put($name, $value)
{
$parsed = explode('.', $name);
$session =& $_SESSION;
while (count($parsed) > 1) {
$next = array_shift($parsed);
if ( ! isset($session[$next]) || ! is_array($session[$next])) {
$session[$next] = [];
}
$session =& $session[$next];
}
$session[array_shift($parsed)] = $value;
}
That's it we now have are secured session system. Now edit our CSRF helper to look like this:
<?php
namespace application\helpers;
use Request;
class CSRF
{
public static $name = '_CSRF';
public function __construct()
{
$this->session = new SecureSession();
$this->session->start();
}
public function insert($form = 'default')
{
echo '<input type="hidden" name="csrf_token" value="' . $this->generate($form) . '">';
}
public function generate($form = NULL)
{
$token = self::token() . self::fingerprint();
$this->session->put(self::$name . '_' . $form, $token);
return $token;
}
public function check($token, $form = NULL)
{
if ($this->session->get(self::$name . '_' . $form) && $this->session->get(self::$name . '_' . $form) === $token) { // token OK
return (substr($token, -32) === self::fingerprint()); // fingerprint OK?
}
return FALSE;
}
protected static function token()
{
mt_srand((double) microtime() * 10000);
$charid = strtoupper(md5(uniqid(rand(), TRUE)));
return substr($charid, 0, 8) .
substr($charid, 8, 4) .
substr($charid, 12, 4) .
substr($charid, 16, 4) .
substr($charid, 20, 12);
}
protected static function fingerprint()
{
return strtoupper(md5(implode('|', array(Request::server('REMOTE_ADDR'), Request::server('HTTP_USER_AGENT')))));
}
}
Now that we have done that we need to ensure that session will be started when website is opened. To do that we will need to create folder named "middleware" in /application/
directory and inside create the file "Persister.php"
<?php
namespace application\middleware;
use application\helpers\SecureSession;
class Persister
{
public static $session;
public static function run()
{
$session = new SecureSession(SESSION_KEY, 'PHPSESSID');
ini_set('session.save_handler', 'files');
session_set_save_handler($session, true);
session_save_path(SESSION_PATH);
$session->start();
}
}
Now we need to register middleware in the core, so let's create Middleware.php inside /application/core/
and write this:
<?php
namespace application\core;
use application\middleware\Persister;
class Middleware
{
public static function persist()
{
Persister::run();
}
}
And finally we need to call th middleware in our root index.php file so change it's contents to the following
<?php
use application\core\Router;
use application\core\Middleware;
define('ROOT_PATH', __DIR__. '/');
require_once ROOT_PATH . 'application/config/config.php';
spl_autoload_register(function($class) {
$path = str_replace('\\', '/', $class.'.php');
if (file_exists($path)) {
require $path;
}
});
if(ENVIRONMENT === 'dev') {
//debugger handler
}elseif(ENVIRONMENT === 'prod'){
error_reporting(0);
ini_set('display_errors', 0);
}else{
die('Fatal error: environment not defined or valid!');
}
Middleware::persist();
$router = new Router;
$router->run();
That's it sessions are up and running. Next up are cookies so create /application/helper/Cookies.php
<?php
namespace application\helpers;
use Request;
class Cookies
{
public $name;
public $value = '';
public $expires;
public $path = '/';
public $domain = '';
public $secure = false;
public $http_only = false;
public function __construct($name, $value = null)
{
$this->name = $name;
$this->domain = '.' .Request::server('SERVER_NAME');
if (Request::server('HTTPS') && Request::server('HTTPS') !== 'off') {
$this->secure = true;
}
if ($value !== null) {
$this->value = $value;
}
}
}
As cookies have expiry date we can create some functions that will make it easier to configure
public function expires_in($time, $unit = "months")
{
if (!empty($time)) {
switch ($unit) {
case 'months' :
$time = $time * 60 * 60 * 24 * 31;
break;
case 'days' :
$time = $time * 60 * 60 * 24;
break;
case 'hours' :
$time = $time * 60 * 60;
break;
case 'minutes':
$time *= 60;
break;
}
}
$this->expires_at($time);
}
public function expires_at($time)
{
if (empty($time)) {
$time = null;
}
$this->expires = $time;
}
And while we are abstarcting, we might as well add methods for reading and writing to a cookie
public function set()
{
return setcookie(
$this->name,
$this->value,
$this->expires,
$this->path,
$this->domain,
$this->secure,
$this->http_only
);
}
public function get()
{
return ($this->value === '' && isset($_COOKIE[$this->name])) ? $_COOKIE[$this->name] : $this->value;
}
public function delete()
{
$this->value = '';
$this->expires = time() - 3600;
return $this->set();
}
And that's the whole cookie class. We can now add it to our middleware if we need. Actually let's do that. Open up the /application/middleware/Persister.php
and edit it like this:
<?php
namespace application\middleware;
use application\helpers\SecureSession;
use application\helpers\Cookies;
class Persister
{
public static $session;
public static $cookies;
public static function run()
{
$session = new SecureSession(SESSION_KEY, 'PHPSESSID');
$cookies = new Cookies('SESSION_COOKIE');
$cookies->expires = 0;
$cookies->value = 'TRUE';
$cookies->set();
ini_set('session.save_handler', 'files');
session_set_save_handler($session, true);
session_save_path(SESSION_PATH);
$session->start();
}
}
So now we have session cookie stored on on browser of the user who visits our webpage.