Sessions in PHP. Control of user inactivity with built-in PHP tools

Website security relies on session management. When a user connects to a secure site, they provide credentials, typically in the form of a username and password. The web server has no idea which user is already logged in and how he navigates from page to page. The session mechanism allows users not to enter a password each time they want to perform a new action or navigate to a new page.

In essence, session management ensures that the user who was authenticated is currently connected. But, unfortunately, sessions have become an obvious target for hackers, as they can allow access to a web server without the need for authentication.

After the user is authenticated, the web server provides the user with a session ID. This identifier is stored in the browser and is substituted whenever authentication is needed. This avoids repetitive login/password entry processes. All this happens in the background and does not cause discomfort to the user. Imagine if you entered your username and password every time you viewed a new page!

In this article, I will try to outline all the ways I know to protect the session ID in PHP.

Using Cookies By default, all session information, including the ID, is passed in a cookie. But this is not always the case. Some users disable cookies in their browsers. In this case, the browser will pass the session ID in the URL.

Here, the ID is passed in the clear, unlike the session through a cookie, when the information is hidden in the HTTP header. The easiest way to protect against this would be to prevent the transfer of the session ID through the address bar. You can do this by writing the following in the Apache server configuration file.htaccess:

php_flag session.use_only_cookies on

Using Encryption If your site needs to handle sensitive information such as credit card numbers (hello from Sony), you should use SSL3.0 or TSL1.0 encryption. To do this, set the cookie to true for the secure parameter.

If you store the session password in the $_SESSION variable (it's still better to use sql), then you should not store it in the clear.

If ($_SESSION["password"] == $userpass) ( // code )

The code above is not secure because the password is stored in plain text in a session variable. Instead, use md5 encryption, something like this:

If ($_SESSION["md5password"] == md5($userpass)) ( // code )

Browser check To block the possibility of using a session from another browser (computer), you should enter a check for the user-agent HTTP header field:

session_start(); if (isset($_SESSION["HTTP_USER_AGENT"])) ( if ($_SESSION["HTTP_USER_AGENT"] != md5($_SERVER["HTTP_USER_AGENT"])) ( // code ) ) else ( $_SESSION["HTTP_USER_AGENT"] = md5($_SERVER["HTTP_USER_AGENT"]); )

Session expiration Limit the lifetime of the session as well as the duration of the cookie. The default session expiration is 1440 seconds. You can change this value through php.ini and .htaccess. Example for .htaccess:

# Session lifetime in seconds
php_value session.gc_maxlifetime 3600
# Cookie lifetime in seconds
php_value session.cookie_lifetime 3600

Binding by IP address In certain situations (not always), you should bind by IP address. Basically when the number of users is limited and have static IP. The check can either be against a list of allowed IP addresses,

include("ip_list.php"); //$ip_white_list = array ("admin1" => "111.222.333.444", "admin2" => "555.666.777.888"); if(!empty(array_search($_SERVER["REMOTE_ADDR"],$ip_white_list))) ( header("Location: admin.php"); ) else ( echo "ACCESS DENY!"; )

Or by IP address for each request (only for static IPs):

If(isset($_SESSION["ip"]) and $_SESSION["ip"] == $_SERVER["REMOTE_ADDR"]) ( header("Location: admin.php"); ) else ( session_unset(); $_SESSION["ip"] = $_SERVER["REMOTE_ADDR"]; )

You should be aware that it is impossible to completely avoid hacking. You can only make this hacking as difficult as possible by any known means. However, you should also not forget about your legal users, so as not to complicate their lives with such protection.

7.7K

With the help of the PHP session, the server identifies you and allows you to perform the necessary operations: changing information on various web pages, adding new information, etc. After completing your work on the site, you delete the current session by clicking on the " Log out»:

What is a PHP session?

A PHP session is a way of storing information in session variables that can be used to authenticate against multiple web pages. Unlike cookies, session information is not stored on the user's computer. Instead, the session creates a file on the server in a temporary directory.

This information, stored throughout the session, is available to all web pages of the resource. On the server, the location of the temporary file is determined by the session.save_path parameter in the php.ini configuration file.

When a PHP session is created, the following three actions are performed:

  • When a session is created, PHP generates a unique identifier, which is a random string of 32 hexadecimal numbers. The PHP session lifetime ID looks something like this: 9c8foj87c3jj973actop1re472e8774 ;
  • The server sends a cookie, called PHPSESSID , to the user's computer to store a unique session identifier string;
  • The server generates a file in the specified temporary directory that contains the name of the unique session identifier with the sess _g prefix. sess_9c8foj87c3jj973actop1re472e8774 .

These settings help the PHP script retrieve session variable values ​​from the file. On the client side, PHPSESSID contains the session ID. It confirms the name of the file to be searched in a certain directory on the server side, from which session variables can be extracted and used for verification.

The user can end the session by pressing the logout button, which calls the session_destroy() function. When the user closes the browser, the PHP session is closed automatically. Otherwise, the server will end the session after the specified period of time.

Session Syntax in PHP

With PHP authorization through a session, it is created using the session_start() function and deleted using the session_destroy() function. The PHP global variable, known as $_SESSION , is used to set session variables. You can reset all values ​​set for session variables using the session_unset() function.

Session operations

We will look at the following operations using a PHP session, as well as examples of them.

  • Starting a PHP session and setting its session variables: A new PHP session is started using the session_start() function. After a session has been created, you can set its session variables with $_SESSION . We have set the values ​​for the variables “ userID" - " php_user" and "password" - "tutorials":

PHP sessions - creation

Result: Running the above PHP code on the server will produce the following message:

  • Retrieving the values ​​of PHP session variables : We can get the values ​​of the variables that we set during the last PHP login session. When we open a PHP session at the beginning of each page ( session_start () ), the code below should be indicated. We retrieve and display these values ​​using the $_SESSION global variable:

PHP session - getting values

Result: When we run the above PHP code on the server, we will get the following message as a result. The values ​​of the session variables that we set earlier, after the PHP session was created, are displayed.

  • Updating the values ​​of PHP session variables : During a session, you can update the values ​​of its variables. First we need to open a PHP session at the beginning of each page (session_start()). In the code below, we update the values ​​of the variables “userID” to “new_php_user” and “password” to “education”.

You can print an array of session variables and their values ​​using the print_r($_SESSION) function, as shown below:

PHP session - changing values

Result: When we run the above PHP code on the server, we will get the following message. It will provide an array of session variables with their new values.

Hello dear community.

First of all, I want to thank you for a very useful resource. More than once I found here a lot of interesting ideas and practical advice.

The purpose of this article is to highlight the pitfalls of using sessions in PHP. Of course, there is PHP documentation and lots of examples, and this article does not pretend to be a complete guide. It is designed to reveal some of the nuances of working with sessions and protect developers from wasting time.

The most common use case for sessions is, of course, user authorization. Let's start with the most basic implementation in order to consistently develop it as new tasks appear.

(In order to save space and time, in the examples we will limit ourselves to the session functions themselves, instead of building a full-fledged test application here with a beautiful class hierarchy, exhaustive error handling, and other right things).

Function startSession() ( // If the session has already been started, stop execution and return TRUE // (the session.auto_start parameter in the php.ini settings file must be disabled - the default value) if (session_id()) return true; else return session_start(); // Note: Prior to version 5.3.0, the session_start() function returned TRUE even in case of an error. // If you are using a version below 5.3.0, perform an additional check session_id() // after calling session_start () ) function destroySession() ( if (session_id()) ( // If there is an active session, delete the session cookies, setcookie(session_name(), session_id(), time()-60*60*24); // and destroy the session session_unset(); session_destroy(); ) )

Note: It is assumed that the reader has basic knowledge of PHP sessions, so we will not cover the principle of operation of the session_start() and session_destroy() functions here. The tasks of layout of the login form and user authentication are not related to the topic of the article, so we will also omit them. Let me just remind you that in order to identify the user in each subsequent request, we need to store the user identifier in the session variable (named userid, for example) at the time of successful login, which will be available in all subsequent requests within the lifetime of the session. It is also necessary to implement processing of the result of our startSession() function. If the function returned FALSE - display the login form in the browser. If the function returned TRUE, and a session variable containing the authenticated user ID (in our case, userid) exists, display the authenticated user's page (for more on error handling, see the addition of 2013-06-07 in the section on session variables).

While everything is clear. Questions begin when it is required to implement control of the lack of user activity (session timeout), to allow several users to work simultaneously in one browser, and also to protect sessions from unauthorized use. This will be discussed below.

Controlling user inactivity with built-in PHP tools The first question that often arises among developers of all kinds of consoles for users is the automatic termination of the session in case of inactivity on the part of the user. There is nothing easier than doing this with PHP's built-in features. (This option is not very reliable and flexible, but we will consider it for the sake of completeness).

Function startSession() ( // User inactivity timeout (in seconds) $sessionLifetime = 300; if (session_id()) return true; // Set the cookie lifetime ini_set("session.cookie_lifetime", $sessionLifetime); // If the user inactivity timeout is set, set the session lifetime on the server // Note: For a production server, it is recommended to preset these parameters in the php.ini file if ($sessionLifetime) ini_set(" session.gc_maxlifetime", $sessionLifetime); if (session_start()) ( setcookie(session_name(), session_id(), time()+$sessionLifetime); return true; ) else return false; )

A few explanations. As you know, PHP determines which session to start based on the cookie name passed by the browser in the request header. The browser, in turn, receives this cookie from the server, where it is placed by the session_start () function. If the cookie in the browser has expired, it will not be passed in the request, which means PHP will not be able to determine which session to start and treat it as creating a new session. The PHP settings parameter session.gc_maxlifetime, which is set to our user inactivity timeout, sets the PHP session lifetime and is controlled by the server. The session lifetime control works as follows (here the example of session storage in temporary files is considered as the most common and default option in PHP).

When a new session is created, in the directory set as the directory for storing sessions in the session.save_path PHP settings parameter, a file is created with the name sess_, where is the session ID. Further, in each request, at the moment of starting an already existing session, PHP updates the modification time of this file. Thus, in each subsequent request, PHP, by the difference between the current time and the last modification time of the session file, can determine whether the session is active, or its lifetime has already expired. (The mechanism for removing old session files is discussed in more detail in the next section.)

Note: It should be noted here that the session.gc_maxlifetime parameter affects all sessions within the same server (more precisely, within the same main PHP process). In practice, this means that if there are several sites running on the server, and each of them has its own user inactivity timeout, then setting this parameter on one of the sites will cause it to be set for other sites. The same applies to shared hosting. To avoid this situation, separate session directories are used for each site within the same server. Setting the path to the session directory is done using the session.save_path parameter in the php.ini settings file, or by calling the ini_set() function. After that, the sessions of each site will be stored in separate directories, and the session.gc_maxlifetime parameter set on one of the sites will only affect its session. We will not consider this case in detail, especially since we have a more flexible option for controlling the inactivity of the user.

User Inactivity Control Using Session Variables It would seem that the previous version, for all its simplicity (just a couple of extra lines of code), gives everything we need. But what if not every request can be regarded as the result of user activity? For example, the page has a timer that periodically makes an AJAX request to receive updates from the server. Such a request cannot be regarded as user activity, which means that the automatic extension of the session lifetime is not correct in this case. But we know that PHP updates the session file modification time automatically every time the session_start() function is called, which means that any request will extend the session lifetime, and the user inactivity timeout will never occur. In addition, the last note from the previous section on the subtleties of the session.gc_maxlifetime parameter may seem too confusing and difficult for someone to implement.

To solve this problem, we will abandon the use of built-in PHP mechanisms and introduce several new session variables that will allow us to control the time of user inactivity on our own.

Function startSession($isUserActivity=true) ( ​​$sessionLifetime = 300; if (session_id()) return true; // Set the cookie lifetime until the browser is closed (we will control everything on the server side) ini_set("session.cookie_lifetime", 0); if (! session_start()) return false; $t = time(); if ($sessionLifetime) ( // If the user inactivity timeout is set, // check the time, then elapsed since the last activity of the user // (the time of the last request when the session variable lastactivity was updated) if (isset($_SESSION["lastactivity"]) && $t-$_SESSION["lastactivity"] >= $sessionLifetime) ( // If the time elapsed since the last activity of the user // is greater than the inactivity timeout, then the session has expired and the session should be terminated destroySession(); return false; ) else ( // If the timeout is not yet step, // and if the request came as a result of user activity, // update the lastactivity variable with the current time value, // thereby extending the session time by another sessionLifetime seconds if ($isUserActivity) $_SESSION["lastactivity"] = $t; ) ) return true; )

Let's summarize. In each request, we check if the timeout has not been reached since the last user activity until the current moment, and if it is, we destroy the session and abort the function, returning FALSE. If the timeout has not been reached, and the $isUserActivity parameter with the value TRUE is passed to the function, we update the time of the last user activity. All we have to do is determine in the calling script whether the request is the result of a user activity, and if not, call the startSession function with $isUserActivity set to FALSE.

Update as of 2013-06-07 Handling the result of the sessionStart() function

The comments drew attention to the fact that returning FALSE does not give a complete understanding of the cause of the error, and this is absolutely fair. I did not publish detailed error handling here (the volume of the article is already not small), since this does not relate directly to the topic of the article. But given the comments, I'll make it clear.

As you can see, the sessionStart function can return FALSE in two cases. Either the session failed to start due to some internal server error (for example, incorrect session settings in php.ini), or the session timed out. In the first case, we must redirect the user to an error page stating that there are problems on the server, and a support contact form. In the second case, we must transfer the user to the login form and display an appropriate message in it that the session time has expired. To do this, we need to enter error codes and return the corresponding code instead of FALSE, and in the calling method, check it and act accordingly.

Now, even if the session still exists on the server, it will be destroyed the first time it is accessed if the user's inactivity timeout has expired. And this will happen regardless of what session lifetime is set in PHP's global settings.

Note: What happens if the browser was closed and the session cookie was automatically destroyed? The request to the server the next time the browser is opened will not contain the session cookie, and the server will not be able to open the session and check the user's inactivity timeout. For us, this is tantamount to creating a new session and does not affect functionality and security in any way. But a fair question arises - who then will destroy the old session, if until now we have destroyed it after the timeout has expired? Or will it now hang in the session directory forever? To clean up old sessions, PHP has a mechanism called garbage collection. It runs at the time of the next request to the server and cleans up all old sessions based on the date the session files were last modified. But the garbage collection mechanism is not triggered with every request to the server. The frequency (more precisely, the probability) of launching is determined by two settings parameters session.gc_probability and session.gc_divisor. The result of dividing the first parameter by the second one is the probability of triggering the garbage collection mechanism. Thus, in order for the session cleanup mechanism to be launched on each request to the server, these parameters must be set to equal values, for example, "1". This approach ensures that the session directory is clean, but is obviously too expensive for the server. Therefore, in production systems, session.gc_divisor is set to 1000 by default, which means that the garbage collection mechanism will run with a probability of 1/1000. If you experiment with these settings in your php.ini file, you may notice that in the case above, when the browser closes and clears all its cookies, there are still old sessions left in the session directory for a while. But this should not worry you, because. as already mentioned, this in no way affects the security of our mechanism.

Update as of 2013-06-07 Preventing scripts from hanging due to session file lock

In the comments, the question was raised about the hanging of simultaneously executing scripts due to the blocking of the session file (as the most striking option - long poll).

To begin with, I note that this problem does not directly depend on the server load or the number of users. Of course, the more requests, the slower the scripts run. But this is an indirect dependency. The problem appears only within one session, when the server receives several requests on behalf of one user (for example, one of them is long poll, and the rest are ordinary requests). Each request tries to access the same session file, and if the previous request did not unlock the file, then the next one will hang pending.

To keep session file locking to a minimum, it is highly recommended to close the session by calling the session_write_close() function immediately after all manipulations with session variables have been completed. In practice, this means that you should not store everything in session variables and refer to them throughout the execution of the script. And if you need to store some working data in session variables, then read them immediately at the start of the session, save them to local variables for later use and close the session (meaning closing the session using the session_write_close function, and not destroying it using session_destroy).

In our example, this means that immediately after opening a session, checking its lifetime and the existence of an authorized user, we must read and save all additional session variables necessary for the application (if any), then close the session by calling session_write_close () and continue script execution, whether it is a long poll or a regular request.

Protecting sessions from unauthorized use Let's imagine a situation. One of your users hooks a trojan that robs the cookies of the browser (in which our session is stored) and sends it to the specified email. The attacker receives the cookie and uses it to forge a request on behalf of our authorized user. The server successfully receives and processes this request as if it came from an authorized user. If additional verification of the IP address is not implemented, such an attack will lead to a successful hacking of the user's account with all the ensuing consequences.

Why did this become possible? Obviously, because the name and identifier of the session are always the same for the entire lifetime of the session, and if you get this data, then you can freely send requests on behalf of another user (naturally, within the lifetime of this session). This may not be the most common type of attack, but theoretically it looks quite feasible, especially considering that such a Trojan does not even need administrator rights to rob a user's browser cookies.

How can you protect yourself from attacks of this kind? Again, obviously, by limiting the lifetime of the session identifier and periodically changing the identifier within the same session. We can also change the session name, completely deleting the old one and creating a new session, copying all the session variables from the old one into it. But this does not affect the essence of the approach, therefore, for simplicity, we restrict ourselves to the session identifier.

It is clear that the shorter the lifetime of the session identifier, the less time an attacker will have to obtain and apply cookies to fake a user request. Ideally, a new identifier should be used for each request, which will minimize the possibility of using someone else's session. But we will consider the general case, when the session ID refresh time is set arbitrarily.

(Let's omit the part of the code that has already been considered).

Function startSession($isUserActivity=true) ( ​​// Session ID lifetime $idLifetime = 60; ... if ($idLifetime) ( // If the session ID lifetime is set, // check the time elapsed since the session was created or last regenerated // (the last request time the session variable starttime was updated) if (isset($_SESSION["starttime"])) ( if ($t-$_SESSION["starttime"] > = $idLifetime) ( // The session ID lifetime has expired // Generate a new ID session_regenerate_id(true); $_SESSION["starttime"] = $t; ) ) else ( // We get here if the session has just been created // Set the session ID generation time to the current time $_SESSION["starttime"] = $t; ) ) return true; )

So, when creating a new session (which happens at the moment of a successful user login), we set the session variable starttime, which stores the time of the last generation of the session identifier for us, to a value equal to the current time of the server. Further, in each request, we check if enough time (idLifetime) has passed since the last generation of the identifier, and if it has passed, we generate a new one. Thus, if during the set lifetime of the identifier, an attacker who has received an authorized user's cookie does not have time to use it, the server will consider the fake request as unauthorized, and the attacker will be taken to the login page.

Note: The new session id goes into the browser's cookies when calling the session_regenerate_id() function, which sends a new cookie, similar to the session_start() function, so we don't need to update the cookies ourselves.

If we want to secure our sessions as much as possible, it is enough to set the lifetime of the identifier to one, or even take the session_regenerate_id () function out of the brackets and remove all checks, which will lead to the regeneration of the identifier in each request. (I have not tested the impact of this approach on performance, and I can only say that the session_regenerate_id(true) function essentially performs only 4 actions: generating a new identifier, creating a header with session cookies, deleting the old one, and creating a new session file).

Digression: If the Trojan turns out to be so smart that it will not send cookies to the attacker, but itself organizes the sending of a pre-prepared fake request immediately upon receiving the cookie, the method described above will most likely not be able to protect against such an attack, because there will be practically no difference between the time the Trojan received the cookie and sending the fake request, and there is a high probability that at this moment the session ID will not be regenerated.

Ability to simultaneously work in one browser on behalf of several users The last task that I would like to consider is the possibility of simultaneous work in one browser of several users. This feature is especially useful during the testing phase when you need to emulate concurrent users, and it is desirable to do this in your favorite browser, rather than using the entire arsenal available or opening multiple browser instances in incognito mode.

In our previous examples, we did not explicitly set the session name, so the default PHP name (PHPSESSID) was used. This means that all the sessions we've created so far have sent cookies to the browser under the name PHPSESSID. Obviously, if the cookie name is always the same, then there is no way within the same browser to organize two sessions with the same name. But if we used our own session name for each user, then the problem would be solved. So let's do it.

Function startSession($isUserActivity=true, $prefix=null) ( ... if (session_id()) return true; // If a user prefix is ​​passed in the parameters, // set a unique session name that includes this prefix, // otherwise set a common name for all users (for example, MYPROJECT) session_name("MYPROJECT".($prefix ? "_".$prefix: "")); ini_set("session.cookie_lifetime", 0); if (! session_start()) return false; ... )

Now the only thing left to do is to ensure that the calling script passes a unique prefix for each user to the startSession() function. This can be done, for example, by passing a prefix in the GET/POST parameters of each request, or through an additional cookie.

Conclusion In conclusion, I will give the complete final code of our functions for working with PHP sessions, including all the tasks discussed above.

Function startSession($isUserActivity=true, $prefix=null) ( $sessionLifetime = 300; $idLifetime = 60; if (session_id()) return true; session_name("MYPROJECT".($prefix ? "_".$prefix: "")); ini_set("session.cookie_lifetime", 0); if (! session_start()) return false; $t = time(); if ($session Lifetime) ( if (isset($_SESSION["lastactivity"]) && $t-$_SESSION["lastactivity"] >= $sessionLifetime) ( destroySession(); return false; ) else ( if ($isUserActivity) $_SESSION["lastactivity"] = $t; ) ) if ($idLifetime) ( if (isset($_SESSION["starttime"])) ( if ($t-$_SESSION ["starttime"] >= $idLifetime) ( session_regenerate_id(true); $_SESSION["starttime"] = $t; ) ) else ( $_SESSION["starttime"] = $t; ) ) return true; ) function destroySession() ( if (session_id()) ( session_unset(); setcookie(session_name(), session_id(), time()-60*60*24); session_de build(); ) )

I hope this article will save some time for those who have never really delved into the mechanism of sessions, and give enough understanding of this mechanism for those who are just starting to get acquainted with PHP.

This article was written in 2009 and remains one of our most popular posts. If you're keen to learn more about PHP and MySQL, you may find this of great interest.

NOTE: This article has been newly updated to work on PHP 4.2 or later!

Recently, I had occasion to work on a small project with a group of people. We had determined early on that email alone wasn't going to be enough to keep everyone in the loop, so I was tasked with building a small Web site for the project. It would contain a simple message board, a place where we could upload documents and other files for the rest of the team to use, and contact information for the various team members.

For many of these features to work, I knew that I’d need users to log in before accessing the relevant parts of the site. What I needed was a system that would let users register for a user ID to access to the site, then immediately use that ID without any intervention on my part.

In this article, I shall provide an overview of the system I developed, beginning in the first half with the user signup process. In the second half, I’ll focus on the site itself, how it requires users to log in and then maintains that logged-in status throughout their visit. I'll be paying special attention to the use of the session management features in PHP. By the end, you should have all the information you need to implement a similar system of your own.

Throughout this article, I’ll be assuming that you have a basic familiarity with the PHP language, the use of forms to submit information to a PHP script, and how PHP may be used to interact with a MySQL database. If any of these are foreign concepts to you, you should begin by reading my previous article, .

Part One: The Signup Process The Signup Form

A natural place to start building a site that will require users to register for access is the registration process itself. As one would expect, a simple Web-based form will do the trick. Here's what it will look like:

And here's the code for this form:




New User Registration



New User Registration Form

* indicates a required field


With the objective now clear, I'll walk you through the code for accesscontrol.php . Begin by including your two handy include files: