I also waste time on the following social networks:

A while back I was reading an article that Chris Shiflett wrote about sessions, it is rather in depth and very informative. He goes into all the details about the HTTP protocol and how it is stateless, how the developer has to add state at the application level. He goes on to talk about how PHP handles storing sessions and why the out of the box solution that PHP offers is not very secure. I suggest you read Chris’s article to better understand how the internal PHP sessions work. One aspect that I dislike about the internal PHP sessions is that they are stored in files on the hard disk (usually /tmp/) by default. This means anyone with access to the machine has access to read the session data. I prefer to store my session information in the database to add an extra layer of security.

Last year I had a talk with Chris about session handling and how to make it more secure. After a lengthy email conversation he brought light into the darkness that overwhelmed me. What I got from the conversation is that there is not much that you can rely on when it comes to making sure the visitor that comes to your site is who they say they are. Most people (as well as myself) would think well can’t you just go by the IP that is returned from $_SERVER[‘REMOTE_ADDR’]? The quick answer to this is no you cannot. Here is the explanation from Chris.

Most notably, a single user can potentially use a different IP address for each request (as is the case with AOL users), and multiple users can potentially use the same IP address (as is the case in many computer labs using an HTTP proxy). These situations can cause a single user to appear to be many, or many users to appear to be one.

Now if you think about it you will smack yourself on the head and say duh, if we base this on the IP anyone coming from a computer lab is on a local network and all computers will have the same IP address. We do not want to make that mistake. If the instructor is logged into their email any one of the users of another computer could potentially takeover the instructors session and read their emails. Obviously this is not a good idea.

So how will we secure the sessions? First you must NEVER react harshly always assume that the session has not been hijacked. You do not want to over react and have it really be a true user who is impacted. We are going to create a custom session handler class skeleton that will at the very least store the sessions in the database. You can find the code below.

The first thing we need to do is create a class with the necessary methods here is the skeleton.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Session
{
    private $ses_id;
    private $db;
    private $table;
 
    public function __construct()
    {
 
    }
 
    public function open($path, $name)
    {
 
    }
 
    public function close()
    {
 
    }
 
    public function read($ses_id)
    {
 
    }
 
    public function write($ses_id, $data)
    {
 
    }
 
    public function destroy($ses_id)
    {
 
    }
 
    public function gc()
    {
 
    }
}

Now that you have the necessary methods you are going to have to put them to use. In order to use a class as a custom session handler you need to instruct PHP that you are going to do that and which methods should be called when necessary. In order to do this we need to create another file (I called this one global.php) that will instantiate the Session class. In this file we are creating an instance of the session class and then calling the php function session_set_save_handler(). You could easily use normal functions for a custom session handler, however because I am using a class you have to pass in an instance of the session object in an array also specifying the class method to use. Below is the code for the file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require_once('session.php');
 
$s = new Session($db);
 
/**
 * Change the save_handler to use
 * the class functions
 */
session_set_save_handler (
    array(&$s, 'open'),
    array(&$s, 'close'),
    array(&$s, 'read'),
    array(&$s, 'write'),
    array(&$s, 'destroy'),
    array(&$s, 'gc')
);

Notice that we are passing an instance of our database object into the session object. We will have to have a reference to this in order to store the sessions in the database. Also note that you are not required to use a database as a storage mechanism. You could very well use txt files stored in a secure directory, maybe even XML files if you wish. You can use whatever you wish for the storage mechanism just make sure that whatever you use is secured.

Now that we have all of that out of the way it is time to talk about what happens when. When you setup a custom session handler the PHP session functions will call the appropriate custom method you define. The method names are a bit self-explanatory however I will cover them here. The open method will be called when you call session_start() this is where we will setup the session for use, things such as setup the database connection. On every page load all of these methods will be called one after another, it will open the session, read the session, write the session, close the session and then call the garbage collector (gc) method to remove all of the expired sessions.

So now that we know what is going to happen when, I think it is time to actually make this thing come to life. Let’s start with the constructor method. All we will do here is store the database connection into the session object for use when reading/writing and cleaning up sessions.

1
2
3
4
5
public function __construct($db, $table = 'sessions')
{
    $this->db = $db;
    $this->table = $table;
}

Next we will implement the open method, we are not going to do a lot in this method. All we are going to do here is set the session lifetime based on the value in the PHP configuration.

1
2
3
4
public function open($path, $name)
{
    $this->ses_life = ini_get('session.gc_maxlifetime');
}

Note that because the session handler requires it we still have to accept the $path and $name as parameters for our method, however since we are using a database for our storage mechanism we are not going to put them to use. All I did in this method was set the session lifetime based on the value in the PHP configuration. You can set this to whatever you would like to however it must be set in seconds.

Moving on we need to implement the close method, all that is done here is a bit of garbage cleanup. We do not want any expired sessions hanging out in the database. If someone closes the browser without the site destroying the session, the data will remain in the database table and will look like we have an open session when in fact we don’t. This method will ensure that the sessions are cleared out.

1
2
3
4
public function close()
{
    $this->gc();
}

We have been talking about the gc method but I have yet to explain what it does. All the gc method does is delete all sessions from the database that have expired due to a timeout. We are figuring this out by calculating the time between the last access of the session subtracted from the current time. If the difference is greater than the session lifetime then we are dealing with an expired session.

1
2
3
4
5
6
7
8
9
10
public function gc()
{
    $ses_life = time() - $this->_ses_life;
    $session_sql = ' DELETE FROM ' . $this->_table. ' WHERE last_access < $ses_life ';
 
    $session_res = $this->db->Query($session_sql);
 
    if (!$session_res) return FALSE;
    else return TRUE;
}

Most of the work takes place in the read and write methods. I admit they are pretty simple as well. Actually there is not really anything overly complex about a custom session handler. The functionality that you will need in the read and write methods are below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public function read($ses_id)
{
    $session_sql = ' SELECT * FROM ' . $this->_table. ' WHERE ses_id = '$ses_id;
    $session_res = $this->_db->Query($session_sql);
    if (!$session_res)
    {
        return '';
    }
 
    $session_num = $this->_db->NumRows($session_res);
 
    if ($session_num > 0)
    {
        $session_row = $this->_db->FetchArray($session_res);
        $ses_data = unserialize($session_row['ses_value']);
        $this->_ses_start = $session_row['ses_start'];
 
        return $this->_ses_id = $ses_id;
    }
    else
    {
        return '';
    }
}
 
public function write($ses_id, $data) 
{
    if(!isset($this->_ses_start)) $this->_ses_start = time();
 
    $session_sql = ' SELECT * FROM ' . $this->_table . ' WHERE ses_id=' . $this->_ses_id;
    $res = $this->_db->Query($session_sql);
    if( $this->_db->NumRows($res) == 0 )
    {
        $session_sql = 'INSERT INTO ' . $this->_table . ' (ses_id, last_access, ses_start, ses_value) VALUES (' . $this->_ses_id . ', ' . time() . ', ' . $this->_ses_start . ', ' . serialize($data) . ')';
    }
    else
    {
        $session_sql = ' UPDATE ' . $this->_table . ' SET last_access=' . time() . ', ses_value=' . serialize($data) . ' WHERE ses_id=' . $this->_ses_id;
    }
 
    $session_res = $this->_db->Query($session_sql);
 
    if (!$session_res) return FALSE;
    else return TRUE;
}

The code for the read and write methods is pretty self-explanatory however I am going to explain what is going on here. When you hit a page it reads the session data for your session id. It grabs the data from the database and it is put into the $_SESSION global array. When the page is finished processing it will call the write method to store the session values into the database.

Now that we have all of that out of the way and the code will work as a basic custom session handler it’s time to talk about the session security and how we should handle the sessions. As was said above you really have no way to know if a person is whom they are reporting they are. You cannot rely on the users IP address because they could be in a computer lab on a local network where all computers are routed through the same proxy. They also could be an AOL user who’s IP can change between page loads due to the proxies that AOL uses.

So if you cannot rely on the IP address to verify the person is whom they say, how can you protect this? As Chris has stated a typical HTTP request includes many optional headers. Here is an example HTTP request:

GET / HTTP/1.1
Host: www.example.org
Cookie: PHPSESSID=12345
User-Agent: Mozilla/5.0 Galeon/1.2.6 (X11; Linux i686; U;) Gecko/20020916
Accept: text/html;q=0.9, */*;q=0.1
Accept-Charset: ISO-8859-1, utf-8;q=0.66, *;q=0.66
Accept-Language: en

Is it safe to assume that if a users browser sends a particular header that it will send it again in subsequent requests? Chris says that with very few exceptions this is true.

However, if a user’s browser does send these headers, is it safe to assume that they will be present in subsequent requests from the same browser? The answer is yes, with very few exceptions.

The first thing we need to do is create the fingerprint We can do this in the session class, we will first create a parameter that will hold the secret key.

1
private $fingerprintKey = 'sdfkj43545lkjlkmndsf89a*(&amp;(Nhnkj2h349*&amp;(';

This is where the system will get a bit more complex. Don’t worry I am going to explain everything in depth. Before we jump into anymore code I am going to explain a bit about what we are going to accomplish. What our application will be doing is basically checking for abnormal activity from a user. We are not going to be assuming that the user will report any information to us and we are not going to require it. When I say this I mean that we are not going to rely on the users IP address and not going to assume that it wont change.

The main goal in securing the session is to watch for abnormal behavior. If a user comes to your site and they are reporting the same User Agent over say 50 page loads and all the sudden it changes, that’s abnormal and we should take action, however it should not be a severe action. If they come to your site and the User Agent is constantly changing, then this is very normal for the user, maybe we should check another value that is reported by the user. I am not going to jump in and try to make this fail proof for you what I am going to do is show you how would can implement this using only 1 value that is reported by the user. We will use the User Agent header which is sent by the browser most of the time. We will discuss what should happen when the user hits the threshold and then the User Agent changes and what you shouldn’t do as well.

For now your session class should look like the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class Session
{
    private $ses_id;
    private $db;
    private $table;
    private $ses_life;
    private $ses_start;
 
    private $fingerprintKey = 'sdfkj43545lkjlkmndsf89a*(&amp;(Nhnkj2h349*&amp;(';
    private $threshold = 25;
    static private $fingerprintChecks = 0;
 
    public function __construct($db, $table = 'sessions')
    {
        $this->db = $db;
        $this->table = $table;
    }
 
    public function open($path, $name)
    {
        $this->ses_life = ini_get('session.gc_maxlifetime');
    }
 
    public function close()
    {
        $this->gc();
    }
 
    public function read($ses_id)
    {
        $session_sql = "SELECT * FROM " . $this->_table. " WHERE ses_id = '$ses_id'";
        $session_res = $this->_db->Query($session_sql);
 
        if (!$session_res) return '';
 
        $session_num = $this->_db->NumRows($session_res);
        if ($session_num > 0)
        {
            $session_row = $this->_db->FetchArray($session_res);
            $ses_data = unserialize($session_row["ses_value"]);
            $this->_ses_start = $session_row['ses_start'];
            return $this->_ses_id = $ses_id;
        }
        else
        {
             return '';
        }
    }
 
    public function write($ses_id, $data) 
    {
        if(!isset($this->_ses_start)) $this->_ses_start = time();
        $session_sql = "SELECT * FROM ".$this->_table." WHERE ses_id='".$this->_ses_id."'";
        $res = $this->_db->Query($session_sql);
        if( $this->_db->NumRows($res) == 0 ) 
        {
            $session_sql = "INSERT INTO ".$this->_table." (ses_id, last_access, ses_start, ses_value) VALUES ('".$this->_ses_id."', ".time().", ".$this->_ses_start.", '".serialize($data)."')";
        }
        else
        {
            $session_sql = "UPDATE ".$this->_table." SET last_access=".time().", ses_value='".serialize($data)."' WHERE ses_id='".$this->_ses_id."'";
        }
 
        $session_res = $this->_db->Query($session_sql);
        if (!$session_res) return FALSE;
        else return TRUE;
    }
 
    public function destroy($ses_id)
    {
 
    }
 
    public function gc()
    {
        $ses_life = time() - $this->_ses_life;
        $session_sql = "DELETE FROM " . $this->_table. " WHERE last_access < $ses_life";
        $session_res = $this->db->Query($session_sql);
        if (!$session_res) return FALSE;
        else return TRUE;
    }
}

Now that we know what we are going to do it is time to look at the code that we will put into the Session class and explain what it does.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function HijackCheck() 
{
    // check the users user agent activity.
    if(isset($_POST['submit'])) 
    {
        if($_SESSION['PW_CHECKS'] < PW_MAX_CHECKS)
        {
            if(isset($_POST['password'])) 
            {
                if(!isset($_SESSION['PW_CHECKS'])) $_SESSION['PW_CHECKS'] = 1;
                if($_SESSION['PW_CHECKS'] <= PW_MAX_CHECKS) 
                {
                    $_SESSION['PW_CHECKS']++;
 
                    // check the password provided, if invalid show the password form again here.
                }
                else 
                {
                    // reset our session variables.
                    unset($_SESSION['UA_CHECKS']);
                    unset($_SESSION['HTTP_USER_AGENT']);
                    unset($_SESSION['PW_CHECKS']);
                }
            }
        }
        else
        {
            $this->destroy($this->_ses_id);
        }
    }
 
    // check to see if UA_CHECKS is instantiated, if not set it to 0
    if(!isset($_SESSION['UA_CHECKS'])) $_SESSION['UA_CHECKS'] = 0;
    // check to see if the users IP address has been set, if not set it.
    if(!isset($_SESSION['HTTP_USER_AGENT'])) $_SESSION['HTTP_USER_AGENT'] = md5($this->fingerprint.$_SERVER['HTTP_USER_AGENT']);
 
    // check to see if the UA has changed
    if($_SESSION['HTTP_USER_AGENT'] == md5($this->fingerprint.$_SERVER['HTTP_USER_AGENT']))
    {
        $_SESSION['UA_CHECKS']++;
    }
    else
    {
        // Check to see if the UA_CHECKS has been completed UA_THRESHOLD times
        // update the new UserAgent
        if($_SESSION['UA_CHECKS'] >= UA_THRESHOLD) 
        {
            // It's not normal for the users UA to change frequently
            $this->validate($pwError);
        }
        else
        {
            unset($_SESSION['UA_CHECKS']);
            unset($_SESSION['HTTP_USER_AGENT']);
        }
    }
}

The first thing we need to do are set our limits. We will need to set a user agent threshold. What is the user agent threshold? The threshold is a number let’s say 25. This is the number that the user needs to reach without the user agent changing. If they reach this number we are going to assume that it is not normal for the user agent for this user to change. On every page load this method will be called and it will check the user agent against the one from the first page load (if there is one) If it does not change it will increment the UA_CHECKS session variable. Once the UA_CHECKS hits the UA_THRESHOLD we know that the user has loaded 25 pages without a change in the user agent. If all of the sudden the user agent changes we know that something went wrong.

However we are going to treat this situation lightly, we are not going to assume that someone hijacked the system. What we will do is just show the user the password form and make them provide their password again. Since we are treating this situation lightly but we also do not want a hijacked session to remain open we will have to set a limit on the password checks. I feel a good number for PW_MAX_CHECKS would be 3. If the user does not provide the correct password after 3 attempts we will destroy the session. Doing this will log the user out of the system and any data stored in the session will be removed.

You will need to add the definitions for the 2 constants to the global.php file. It should now look like the following.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
define( 'UA_THRESHOLD', 25 );
define( 'PW_MAX_CHECKS', 3 );
require_once('session.php');
 
$s = new Session($db);
 
/**
 * Change the save_handler to use
 * the class functions
 */
session_set_save_handler (
    array(&$s, 'open'),
    array(&$s, 'close'),
    array(&$s, 'read'),
    array(&$s, 'write'),
    array(&$s, 'destroy'),
    array(&$s, 'gc')
);

So now if someone is browsing the site while logged in, and something out of the ordinary happens it will prompt the user for their password and if they do not provide the correct password after 3 attempts it will destroy the session. Handling the situation lightly like this allows us to not assume anything and only react when there is definitely a problem. If someone comes to the site and the user agent changes every 5 pages we will assume that is normal and we do not do anything. You could take this class quite a bit further such as checking the users IP after the user fails the user agent checks but I think that would be overkill.

You can also take this further because now you have a custom session handler you can log what browser each user is using, the IP and keep your site’s own traffic stats if you wanted. I have this class extended to show what type of user the person is (Guest, Client, Admin) and I was even logging the pages that they have navigated and the time on each page. Logging this in the admin interface would allow me to see which pages were most important to my users and also see how long the average user would remain on my site. There are many directions you can take this, let me know which direction you go.