In this article I aim to explain the reasons why PHP sessions expire after a set period of inactivity, and how this mechanism works. I'll describe two commonly used methods to control the lifetime of a session, and show how to do so in your code.
The problem
PHP's session mechanism allows us to store data for clients on the server to make it persist through multiple requests. However, because the data itself is stored on the server, and the only thing connecting client's to their respective sessions on the server is a simple cookie, this creates a major security concern.
An exploit known as session hijacking is when a malicious third party intercepts or steals the session cookie from a client, and uses it to access the data from an active session. The server can do little to defend against this because it has no way to tell the original client from the hijacker.
The solution
The most important defense we have against this is to disregard sessions that have been inactive for a set period. This prevents hijackers from accessing sessions unless they are doing so in real-time, within the specified period.
In PHP the mechanism that implements this is known as the session garbage collector. Data files are tagged with a "last modified" time stamp, which PHP uses to determine when the client last used a session. If the session is older than the allowed lifetime of a session, it is destroyed. The default lifetime of a session in PHP is 1440 seconds, or 24 minutes.
We may find ourselves in situations where we would want to manually configure the session lifetime; to be able to better control the time-out period of a session. There are two ways we can do that:
Method #1: Code it manually
You can simply add a snippet of code to the top of every page on your site that sets a field in the session to the current time. You can then use that field to determine how long the session has stayed inactive for and act accordingly. For example:
Expand|Select|Wrap|Line Numbers
- session_start();
- $timeout = 60; // Number of seconds until it times out.
- // Check if the timeout field exists.
- if(isset($_SESSION['timeout'])) {
- // See if the number of seconds since the last
- // visit is larger than the timeout period.
- $duration = time() - (int)$_SESSION['timeout'];
- if($duration > $timeout) {
- // Destroy the session and restart it.
- session_destroy();
- session_start();
- }
- }
- // Update the timout field with the current time.
- $_SESSION['timeout'] = time();
Method #2: Reconfigure PHP's session garbage collection
The session.gc_maxl ifetime PHP.ini directive controls how long a session is allowed to exists before it is considered garbage and is cleaned up. Every call to the session_start() function has a chance to trigger the garbage collection routine.
The chance each call has to trigger the routine is determined by the session.gc_prob ability and session.gc_divi sor directives. The probability is calculated as:
- session.gc_prob ability / session.gc_divi sor
By default the values are 1 and 100, respectively, or a 1% chance that the garbage collector is triggered. If your site has low traffic, you should increase the value of session.gc_prob ability to increase the chance. If you want to guarantee that it will be triggered, set both directives to the same value.
Another thing to consider is the directory where PHP stores the session data. PHP applies the same lifetime to all session data files in the same directory; the lowest lifetime applies to them all. Meaning that if you want to be sure your session is timed out correctly, you need to be using your own directory. Best way to deal with that is to either create one in your home or temp directories. You can have PHP create the directory using the mkdir function, and you set the location by changing the session.save_pa th directive.
These values can be set using the ini_set function, so you can set the session garbage collection values on a per-request basis. For example, this function can be used to start a session and have it time out using a given time-out value:
Expand|Select|Wrap|Line Numbers
- <?php
- /***
- * Starts a session with a specific timeout and a specific GC probability.
- * @param int $timeout The number of seconds until it should time out.
- * @param int $probability The probablity, in int percentage, that the garbage
- * collection routine will be triggered right now.
- * @param strint $cookie_domain The domain path for the cookie.
- */
- function session_start_timeout($timeout=5, $probability=100, $cookie_domain='/') {
- // Set the max lifetime
- ini_set("session.gc_maxlifetime", $timeout);
- // Set the session cookie to timout
- ini_set("session.cookie_lifetime", $timeout);
- // Change the save path. Sessions stored in teh same path
- // all share the same lifetime; the lowest lifetime will be
- // used for all. Therefore, for this to work, the session
- // must be stored in a directory where only sessions sharing
- // it's lifetime are. Best to just dynamically create on.
- $seperator = strstr(strtoupper(substr(PHP_OS, 0, 3)), "WIN") ? "\\" : "/";
- $path = ini_get("session.save_path") . $seperator . "session_" . $timeout . "sec";
- if(!file_exists($path)) {
- if(!mkdir($path, 600)) {
- trigger_error("Failed to create session save path directory '$path'. Check permissions.", E_USER_ERROR);
- }
- }
- ini_set("session.save_path", $path);
- // Set the chance to trigger the garbage collection.
- ini_set("session.gc_probability", $probability);
- ini_set("session.gc_divisor", 100); // Should always be 100
- // Start the session!
- session_start();
- // Renew the time left until this session times out.
- // If you skip this, the session will time out based
- // on the time when it was created, rather than when
- // it was last used.
- if(isset($_COOKIE[session_name()])) {
- setcookie(session_name(), $_COOKIE[session_name()], time() + $timeout, $cookie_domain);
- }
- }
Expand|Select|Wrap|Line Numbers
- session_start_timeout(60);
Expand|Select|Wrap|Line Numbers
- session_start_timeout(60, 10);
Epilogue
If you have any questions or comments about the article, feel free to leave a reply. If you need help fixing specific problems, I urge you to post a new question in the PHP answers forum, where the entire community will do it's best to help you out.
All the best,
- Atli