Working with Sessions and Cookies in PHP and MYSQL

Security is an essential component for any site to be successful. However, adding security can increase overhead and annoy your users if not done properly. In this tutorial, I will discuss common problems that are encountered when using sessions and cookies.

Cookies

Cookies are ways for the user to be identified by a website. For instance, a site will create a cookie with the name: “favorite_color” and the value: “red”. Now, each time that you visit the site, it will load the cookie and establish that your “favorite_color” is “red”. This can be quite effective in remembering users, so a user does not have to log in each time they visit your site. The “Remember Me”, seen in many sites, uses this method. However, this can also create some serious security holes.

One example is not verifying what the cookie returns to your site. After some simple searching, I found a website with a user panel script. It output all my login information in cookies and loaded the values each time I preformed an operation. For instance, if I left a comment, it would output whatever my name was from the cookie name variable. Now using firebug and some add-ons, I could not only view these cookies but also edit them. So every time I edited the cookie containing my name, the site would not validate my name but just output it instead. Also I could change my user ID, and eventually was able to find the user ID of the admin (001) and gain full admin privileges.

<?php
// Setting a cookie in PHP is easy
set_cookie(name, value, expiration, path, domain, secure, http_only);
 
// Name is what your cookie will be stored as.
// Value is the value that will be stored in your cookie.
// Expiration is when your cookie will expire (delete).
// Path is the path on the server the cookie will be available to.
// Domain is the domain that the cookie is available to.  (Note: www.example.com counts www as a subdomain of example.com)
// Secure is if the cookie will be only transferred over a secure connection SSL.
// HTTP_only will send the cookie only through HTTP protocol.  (Note: only available for PHP 5.2 +, and on certain browsers)
 
?>

Sessions

Sessions are variables that are stored on your server to identify a user. As opposed to cookies, users cannot directly modify them – but there are still security risks involved. There are two main threats for sessions: Session Fixation and Session Hijacking.

Session Fixation

Session Fixation is when a user goes to an already established session and loads their information into that session. By going into an already established session, the attacker can visit that session and gain the information that the user has inputted. A simple example of this is if you click on a link of a website where the session id has already been established. Such as, http://www.example.com/?PHPSESSID=1234. Now the attacker knows to go to the same PHPSESSID to view your information.

Session Hijacking

Session Hijacking is the second type of risk, and is much harder to defend against. This is where the attacker can obtain your session id by packet sniffing or various other methods. For example, the attacker can be connected into your network and filter all of your data that is being sent to your router. Once your session ID has been obtained, he can visit that ID to gain access to all of your information.

<?php
// Working with sessions is quite easy, first thing you have to do on everypage is begin the session.
session_start();
// Then you can set the variables for the user
$_SESSION['username'] = "nettuts";
$_SESSION['website_url'] = "www.net.tutsplus.com";
 
echo $_SESSION['username']; // Will echo "nettuts".
 
?>

Using Sessions Effectively

Coding a login script is outside the parameters of this tutorial. But I will show you how to make your current login script more secure.

Using sessions is generally more secure than using cookies. This is because users cannot change session values as easily as they can change cookie values. This is why I like to store all my user variables in session variables. Another important tip is to never trust user input. You always want to verify what the user inputs with values in your MYSQL database and then output to the session accordingly. Consider changing your login function to something similar to the following function:

<?php
 
function login($username, $password)
		$sql = mysql_query("SELECT id, user_level FROM users WHERE password = '" . $password . "' AND username = '" . $username . "' LIMIT 1");
		// If there are no matches then the username and password do not match
		if($sql === false) 
		{
			return false;
		}
		else
		{
			while($u = mysql_fetch_array($sql))
			{ 
                session_regenerate_id(true);
                $session_id = $u[id];
                $session_username = $username;
                $session_level = $u[user_level];
 
                $_SESSION['user_id'] = $session_id;
                $_SESSION['user_level'] = $session_level;
                $_SESSION['user_name'] = $session_username;
                $_SESSION['user_lastactive'] = time();
				return true;
			}
		}
 
?>

Let’s analyze this code. It takes the username and password, and checks if there is any user where both criteria match. If nothing returns, then there is an invalid username/password combination and it returns false. Otherwise it creates session variables user_id, user_level, user_name, and user_lastactive. And inputs data from the mysql table in those values.

Now you might be wondering what that function “session_regenerate_id(true)” is. Earlier we talked about session fixation. This is a solution to help defend against that. It creates a new session id every time a user is logged in. So now if a user clicks a link with a set session value it will generate a new session id, and the users info will be added to the new session and not the old one. Passing the parameter true through the function deletes the old session and removes any information.

Writing a Remember Me Function

A cookie or a session should never contain a user’s password. This is primarily because if a cookie or session is hijacked, you do not want the hijacker to have total control over the account. Also users have been known to use the same password for numerous accounts so this could give the attacker control over accounts on different sites. So how can we solve this dilemma?

The solution to this problem is having an auth_key. An auth_key can be the strings of the username, password, and a random string combined and then encrypted. Auth_keys should also be unique for each user. This way, when a cookie is set, you set the value to auth_key and then you check the auth_key value with a value in the MySQL table that you will add. Let’s take a look at how our user login function will change.

<?php
 
function login($username, $password, $remember = false) 
{
    $sql = mysql_query("SELECT * FROM bio_users WHERE password = '" . $password . "' AND username = '" . $username . "' LIMIT 1");
    // If there are no matches then the username and password do not match
    if($sql === false) 
    {
        return false;
    }
    else
    {
        while($u = mysql_fetch_array($sql))
        { 
                $this->account_active = true;
                // Check if user wants account to be saved in cookie
                if($remember)
                {
                    // Generate new auth key for each log in (so old auth key can not be used multiple times in case 
                    // of cookie hijacking)
                    $cookie_auth= rand_string(10) . $username;
                    $auth_key = session_encrypt($cookie_auth);
                    $auth_query = mysql_query("UPDATE users SET auth_key = '" . $auth_key . "' WHERE username = '" . $username . "'");
 
                    setcookie("auth_key", $auth_key, time() + 60 * 60 * 24 * 7, "/", "example.com", false, true)
                }
                // Assign variables to session
                session_regenerate_id(true);
                $session_id = $u[id];
                $session_username = $username;
                $session_level = $u[user_level];
 
                $_SESSION['user_id'] = $session_id;
                $_SESSION['user_level'] = $session_level;
                $_SESSION['user_name'] = $session_username;
                $_SESSION['user_lastactive'] = time();
				return true;
        }
    }
}   
        ?>

Now this checks if “true” is passed through the remember parameter in the login function. If it is, it will set a cookie for auth_key. Rand_string will generate a string in length of the number that is passed through it. Session_encrypt will add a salt to the string and then encrypt it using md5. This string will be unique because it uses the user’s username, which should be unique for each person. So now it sets the auth_key in the mysql table to the value it enters in the cookie. However, this might not be enough protection for your site, so you might consider adding multiple auth_keys and multiple cookies. However this function is not sufficient enough for a remember me function. We must also add an initiate function.

This function should be called on every page. Its purpose is to check for the cookie “auth_key”. If the cookie isset then it will bring up the appropriate user. Once again, this function must be included on every page.

<?php
function initiate()
{
 
		$logged_in = false;
		if(isset($_SESSION['user_name']))
		{
			$logged_in = true;
		}
 
        // Check that cookie is set
        if(isset($_COOKIE['auth_key']))
        {
            $auth_key = safe_var($_COOKIE['auth_key']);
 
            if($logged_in === false)
            {
                // Select user from database where auth key matches (auth keys are unique)
                $auth_key_query = mysql_query("SELECT username, password FROM users WHERE auth_key = '" . $auth_key . "' LIMIT 1");
                if($auth_key_query === false)
                {
                    // If auth key does not belong to a user delete the cookie
                    setcookie("auth_key", "", time() - 3600);
                }
                else
                {
                    while($u = mysql_fetch_array($auth_key_query))
                    {
                        // Go ahead and log in
                        login($u['username'], $u['password'], true);
                    }
                }
            }
            else
            {
                setcookie("auth_key", "", time() - 3600);
            }
        }
 
}
 
    ?>

This code checks if the cookie isset. If the cookie isset, then it will check that the auth_key matches with a user. If it matches with a user then it will pull the required information and log the user in. Otherwise it will delete the cookie because it is invalid.

Other Functions

<?php
 
function logout()
	{
		// Need to delete auth key from database so cookie can no longer be used
		$username = $_SESSION['user_name'];
		setcookie("auth_key", "", time() - 3600);
				$auth_query = mysql_query("UPDATE users SET auth_key = 0 WHERE username = '" . $username . "'");
		// If auth key is deleted from database proceed to unset all session variables
		if ($auth_query)
		{
			unset($_SESSION['user_id']);
			unset($_SESSION['user_level']);
			unset($_SESSION['user_name']);;
			unset($_SESSION['user_lastactive']);
			session_unset();
			session_destroy(); 
            return true;
		}
		else
		{
			return false;
		}
	}
 
 
	// Check if session is still active and if it keep it alive
	function keepalive()
	{
		// If session is supposed to be saved or remembered ignore following code
		if(!isset($_COOKIE['auth_key'])
		{
			$oldtime = $_SESSION['user_lastactive'];
			if(!empty($oldtime))
			{
				$currenttime = time();
                // this is equivalent to 30 minutes
				$timeoutlength = 30 * 600;
				if($oldtime + $timeoutlength >= $currenttime){ 
					// Set new user last active time
					$_SESSION['user_lastactive'] = $currenttime;
				}
				else
				{
					// If session has been inactive too long logout
					logout();
				}
			}
		}
	}
 
   ?>

The logout function is self explanatory. It will delete all variables in the session, delete the cookie and set the auth_key for the user to 0 so the auth_key can no longer be used. It should be noted that the user can set his cookie to 0 and still login as that user. To fix this validate all information that is received from the cookies. Use regexp to make sure the string is the valid length, contains the valid characters, ect. The function keepalive() keeps the session active. This function should be included on every page

Conclusion & Session Hijacking

Session Hijacking is extremely complicated to protect against. You might have read some suggestions to use a combination of the user’s IP address or User Agent to create a fingerprint. However, this is all ineffective to your actual users. Users IP’s change all the time; for big ISPs, such as AOL, they change every few minutes. This would create a huge problem. User Agents can also change — it has been recorded that, in IE7, the user agent can change at times. The best way to protect against hijacking is to write a token system. This is a system that will output a cookie on each page load and store that value also in your mysql table. It will then match the cookie value with the MySQL table value. If they are not equal, then the session will be invalidated

These are some basic functions to teach you how to work with sessions and cookies. Of course, for more protection, you need to add more cookies to validate against. This level of protection is not enough for protecting extremely valuable information, but it should get you started! Some closing notes:

  • Always ensure that cookie values are valid.
  • Never output a password to a session or cookie variable.
  • Use session_regenerate_id to prevent against session fixation.

    • 29

      Comments
      • Joel Vardy says:

        Excellent explanation and demos, the more people who understand this and build secure websites the better.
        Thanks!

      • OrganicIT says:

        Great Tut, Being new to php I am glad such resources are available to help get me on the right track

      • Seich says:

        Really nice tutorial. You can learn a lot from it.

      • Great tutorial, this one I’ll be printing and taking to starbucks so I can really go over it.

        Speaking of which, I think this blog’s print style could use some improvement, the first page is always just an ugly stack of images and code snippets always get cut off.

      • Daft Viking says:

        Good concepts here regarding cookies and sessions, but there are a couple things that should be addressed before using this sample code in a production environment (and trust me, someone out there will do a straight copy/paste of this code into their project):

        1. SQL injection (this is a huge issue!): every SQL query in the sample code simply concatenates variables such as $username directly into a query, which leaves it open to SQL injection attacks that could allow the attacker to log in with someone else’s credentials, or alter/delete data in your database. Either sanitize the input variables properly (at the very least, escape single quote characters), or (my preferred method) use the mysqli or PDO database interfaces that support parameterized queries. More specific information is available at http://en.wikipedia.org/wiki/SQL_injection

        2. Plain-text passwords (not as bad as SQL injection, but still fraught with problems): in my (not always) humble opinion, storing passwords in a database in plain text is a bad idea. Despite advice to the contrary from security experts, most internet users will use the same password for just about every site they have an account on. If your database is compromised somehow (SQL injection attack, bad database permissions, stolen/misplaced backup media, etc.) then not only does the attacker have the usernames and passwords for every one of your users, but chances are good that they will be able to access your users’ other accounts on other systems. There’s a good discussion on it at http://www.codinghorror.com/blog/archives/000953.html

        I hope this isn’t coming across as overly critical – this is just a pet peeve of mine, as I’ve seen far too much sample code online with obvious SQL injection and other types of vulnerabilities.

      • Revell says:

        Wow, nice! I’ve been wondering how to do an auto logout function! So simple!

      • Kevin says:

        Hi, just a question but first, congratulation for your article, it was very interesting.

        At the end of your function initiate, you delete the cookie if the user has a session and the auth_key cookie set, why? It isn’t on the contrary the time reinitialize it with setcookie(“auth_key”, $auth_key, time() + 60 * 60 * 24 * 7, “/”, “example.com”, false, true).

        Thanks a lot.

      • BroOf says:

        Wow this helps a lot!

      • Jônatan Fróes says:

        Very well explaned! congrats.

      • Meshach says:

        So how secure is this?

        I’ve been nervous to start learning sessions before I can discern what is secure and what is not secure..

      • Zac says:

        Excellent work here! This really broadened my scope on login security.

      • krike says:

        God bless you, I really needed this for my Cms tutorial site I just made. I wasn’t sure if I should use cookie or only sessions.

        I do have one question, my php teacher told me to look for a way to remember sessions without cookies, is that possible?

      • Chris says:

        I had be wondering / waiting for a sessions and cookies tutorial with some decent examples and capabilities.

        Great tut.

        Chris

      • Aditya Gandra says:

        @krike:

        Yeah, you can do sessions without cookies but it becomes a huge pain. There are two possible ways to do it. One is passing the session ID through the URL (as a get variable). The other is passing the session ID through a hidden input field (as a post variable). There are problems with both methods and both are generally inconvenient to do.

        One problem with passing it through a URL is security. What is the user copies and pastes a page link to a friend or someone else? Now that friend possibly has access to the users session.

        Also, when you use these methods you will most likely be regenerating a new session ID every couple pages. This could be a problem is the user uses the back button on their browser, because then the session ID could be expired.

        This is why a lot of sites require cookies to login. It is generally a much more pain free experience. I dont have a list of sites that require cookies to login but I know one major example is Gmail (requires cookies enabled to login).

      • GaVrA says:

        Excellent text! But i guess that you can’t ever be 100% safe…

      • Çağlar says:

        Also in addition to what you’ve said never ever save passwords in cleartext in the DB.

      • Dominik says:

        By the way:

        $sql = mysql_query("SELECT * FROM bio_users WHERE password = '" . $password . "' AND username = '" . $username . "' LIMIT 1");

        Please, please: Never do it like that.

        That’s the way to go:

        $sql = mysql_query("SELECT * FROM bio_users WHERE password = '" . mysql_escape_string($password) . "' AND username = '" . mysql_escape_string($username) . "' LIMIT 1");

        ALWAYS escape everything you put into SQL queries. ALWAYS.

        Did I mention…? ALWAYS!

        And when we’re at it: Don’t store passwords in plain text. At least use SHA1 or – even better – use SHA1 and apply a SALT.

        I know that this article is about sessions and cookies, but people will use this code if you don’t tell them that it’s not ready to manufacture.

      • Henrik says:

        This is a joke, you cant write code examples and tell people to write something like this, when it uses internally self created functions.

        session_encrypt dosent exists
        rand_string dosent exists

        come on?!

      • Aayush says:

        This is very helpful…specially the remember me function and plus it refreshed my memory with the basics…

      • Aditya Gandra says:

        Hey, I thought that data sanitization was outside of the scope of this article.
        But, here is a nice example you can read on at nettuts:
        http://net.tutsplus.com/tutorials/tools-and-tips/can-you-hack-your-own-site-a-look-at-some-essential-security-considerations/

        @Henrik:

        Session_Encrypt combines the input with a salt and md5’s it.
        So it would look something like:

        function session_encrypt($string) {
        $salt = ‘asdfsdfsdfa’;
        $string = md5($salt . $string);
        }

        and rand_string just generates a random string.

        Once again please always check data that is submitted to you by the user, and always sanitize the data!

        The examples in this were meant to show you how to do certain things in relationship to using cookies and sessions, this is not production level code.

      • Excellent Tuts! very nice!

      • J.R. says:

        Another good secure idea is to use the secure flag on your cookies, even ones with session. Also not a bad idea to use HTTPOnly but ti depends on what you are doing.

      • onnay okheng says:

        that’s excellent..
        thank’s… ;)

      • You can also add IP checking to this method. While user’s IP address do change, they are not likely to change in the middle of a session. So adding an IP field to the database, and checking that the IP is the same for a session is an additional check against session hijacking. I added an article below that uses the IP field as well.

        Here is the article, look at the cookie/session model (NOT the HTTP/HTTPS model). It uses oracle for the DB, but it’s very easily converted to MySQL: http://www.oracle.com/technology/pub/articles/mclaughlin-phpid1.html

        ADDITIONAL NOTES (IMPORTANT!!!!):

        As others have stated, the author doesn’t touch on SQL injection attacks, and they’ve posted sanitation methods. There is also another method: prepared statements. Look up mysqli prepare, it does all the sanitation for you :)

        Also, this article and the one I’ve provided use either MD5 or sha-1. Both of these encryption methods have been hacked. It’s best to use something stronger, such as sha-256, sha-512, or whirlpool for example.

      • Roman Cortes says:

        Please, correct that insecure mysql injectable code!

        Why are you writing an article on security if you don’t take the time to make the sample code secure!?

        I’m sure your blog have a lot of readers… please take care of them by correcting the unsafe samples soon…

        Also, be free to delete this comment, I just wanted to show my worries to you.

      • I wrote a session class that manages sessions, some people could probably learn from it.

        http://www.milesj.me/resources/script/session-manager

      • Rocky says:

        Quote:

        function session_encrypt($string) {
        $salt = ‘asdfsdfsdfa’;
        $string = md5($salt . $string);
        }

        Dont u mean

        function createsomesortofhash($fromthisstring){
        $salt = “sosdldsldsldsksdklds”;
        $string = md5($salt,$string);
        }

        because you are using the salt at the start of the string!?

      • David Riveroll says:

        This is just what I needed.
        I’d like more security related tutorials as this one, I’ve never had any worries cause my websites weren’t worth hacking, but now I’m getting bigger projects and I want to be prepared.

        Thanks a lot for your input Aditya

      • Vonnero says:

        Great tutorial…..thanks, please keep it up….. on sanitizing database inputs… here is a tutorial for sanitizing any input on the go…..

        http://ilogical.wordpress.com/2009/09/19/form-validation-made-easy/

        contributions or refactoring is highly appreciated….