
Greetings, dear community.
First of all, I want to thank for a very useful resource. Not once 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 example of using 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, we limit ourselves to the examples only by the functions of working with sessions, instead of building here a complete test application with a beautiful hierarchy of classes, exhaustive error handling and other regular things).
function startSession() {
Note: It is implied that the reader has basic knowledge about PHP sessions, so the principle of the work of the session_start () and session_destroy () functions will not be covered here. The tasks of the layout of the login form and user authentication are not related to the topic of the article, so we will also omit them. I recall only that in order to identify the user in each subsequent request, we need at the time of successful entry to store in the session variable (with the name userid, for example) the user ID, which will be available in all subsequent requests within the session lifetime. It is also necessary to implement the processing of the result of our startSession () function. If the function returned FALSE - display the login form in the browser. If the function returns TRUE, and the session variable containing the identifier of the authorized user (in our case, the userid) exists - display the page of the authorized user (for more information about error handling, see the addition of 2013-06-07 in the section on session variables).So far, everything is clear. Questions begin when it is required to implement control of the user’s inactivity (session timeout), to enable several users to work simultaneously in one browser, and also to protect sessions from unauthorized use. This will be discussed below.
Control user inactivity with PHP built-in tools
The first question that often arises from the developers of various consoles for users is the automatic end of the session in case of inactivity on the part of the user. There is nothing easier than to do this with the built-in capabilities of PHP. (This option is not very reliable and flexible, but consider it to complete the picture).
function startSession() {
A little bit of explanation. As you know, PHP determines which particular session to launch, by the name of the cookie sent by the browser in the request header. The browser, in turn, receives this cookie from the server, where the session_start () function places it. If the cookie has expired in the browser, it will not be transmitted in the request, which means that PHP will not be able to determine which session to launch, and will regard this as the creation of a new session. The PHP parameter session.gc_maxlifetime, which is set equal to our timeout of user inactivity, sets the lifetime of the PHP session and is controlled by the server. The session lifetime control works as follows (here we consider an example of the session storage in temporary files as the most common and default option in PHP).
At the time of creating a new session in the directory set as the directory for storing sessions in the PHP settings parameter session.save_path, a file is created named sess_ <sessionid>, where <sessionid> is the session identifier. Further, in each request, at the time of launching an existing session, PHP updates the modification time of this file. Thus, in each subsequent PHP request, the difference between the current time and the time of the last modification of the session file can determine whether the session is active or if its lifetime has already expired. (The mechanism for deleting 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 one server (more precisely, within one 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 installing this parameter on one of the sites will result in its installation 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. The path to the session directory is set 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 be effective only in its sessions. We will not consider this case in detail, especially since we have a more flexible option in place to control the user's inactivity.Control user inactivity 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 executes an AJAX request for receiving 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 any request will prolong the session lifetime, and the user’s inactivity timeout never occurs. In addition, the last note from the previous section on the subtleties of the session.gc_maxlifetime parameter may seem to someone too complicated and difficult to implement.
To solve this problem, we will abandon the use of the built-in PHP mechanisms and introduce several new session variables that will allow us to control the time of inactivity of users on our own.
function startSession($isUserActivity=true) { $sessionLifetime = 300; if ( session_id() ) return true;
To summarize In each request, we check whether the timeout has not reached since the last activity of the user until the current moment, and if it is reached, we destroy the session and interrupt the execution of the function, returning FALSE. If the timeout is not reached, and the $ isUserActivity parameter with the value TRUE is passed to the function - we update the time of the user's last activity. All we have to do is to determine in the calling script whether the request is the result of user activity, and if not, call the startSession function with the $ isUserActivity parameter set to FALSE.
Update as of 2013-06-07
Processing the result of the sessionStart () function
The comments noted that returning FALSE does not provide a complete understanding of the cause of the error, and this is absolutely true. I did not publish detailed error handling here (the length of the article is not so small anyway), since this is not directly related to the topic of the article. But given the comments, I will clarify.
As you can see, the sessionStart function can return FALSE in two cases. Either the session failed to start due to some internal server errors (for example, incorrect session settings in php.ini), or the session has expired. In the first case, we have to transfer the user to the page with an error that there are problems on the server and the form of contacting the support service. In the second case, we need to transfer the user to the login form and display in it the corresponding message that the session 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 on the server still exists, it will be destroyed when it is first accessed, if the user’s inactivity timeout expires. And this will happen regardless of how long the session lifetime is set in the global PHP settings.
Note: And what happens if the browser has been closed and the cookie with the name of the session has been automatically destroyed? The request to the server the next time you open the browser will not contain the session cookie, and the server will not be able to open the session and check the timeout of the user's inactivity. For us, this is equivalent to the creation of a new session and does not affect the functionality and security. But a fair question arises - who then will destroy the old session, if we have so far destroyed it after the timeout expires? Or will it now hang in the session directory forever? For cleaning old sessions in PHP there is a mechanism called garbage collection. It runs at the time of the next request to the server and cleans all old sessions based on the date of the last change of the session files. But the launch of the garbage collection mechanism does not occur with every request to the server. The frequency (or more precisely, the probability) of the launch is determined by the two parameters of the session.gc_probability and session.gc_divisor settings. The result of dividing the first parameter by the second is the probability of launching the garbage collection mechanism. Thus, in order for the session-clearing mechanism to start at each request to the server, these parameters must be set to equal values, for example, "1". This approach ensures the purity of the session directory, but, obviously, is too expensive for the server. Therefore, in production systems, the default value is session.gc_divisor, equal to 1000, which means that the garbage collection mechanism will start with a probability of 1/1000. If you experiment with these settings in your php.ini file, you will notice that in the case described above, when the browser closes and clears all of your cookies, the old sessions are still in the session directory for a while. But this should not worry you, because as has already been said, this in no way affects the safety of our mechanism.Update as of 2013-06-07
Prevent scripts freeze due to session file locking
The comments raised the question of the hang of simultaneously running scripts due to the blocking of the session file (as the brightest option is long poll).
For a start, 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 are executed. But this is a direct dependence. The problem appears only within the same session, when the server receives several requests on behalf of one user (for example, one of them is long poll, and the rest are normal requests). Each request attempts to access the same session file, and if the previous request did not unlock the file, the next one will hang pending.
To keep session files to a minimum, it is strongly recommended to close the session by calling the session_write_close () function immediately after all actions on the session variables have been completed. In practice, this means that you should not store everything in session variables and contact them throughout the execution of the script. And if you need to store some working data in session variables, then read them immediately at session start, save to local variables for later use and close the session (meaning closing the session using the session_write_close function, and not destroying using session_destroy).
In our example, this means that immediately after opening the session, checking the time of its life and the existence of an authorized user, we must read and save all additional session variables necessary for the application (if any exist), then close the session by calling session_write_close () and continue script execution, be it long poll or a regular request.
Protect sessions from unauthorized use
Imagine a situation. One of your users catches a Trojan that robs the browser’s cookies (in which our session is stored) and sends it to the specified email. The attacker receives cookies and uses it to fake a request on behalf of our authorized user. The server successfully accepts 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 is this possible? Obviously, because the name and session identifier are always the same for the entire session lifetime, and if you get this data, you can send requests on behalf of another user (naturally, within the lifetime of this session). Perhaps this is not the most common type of attack, but theoretically everything looks quite realizable, especially considering that such a trojan does not even need administrator rights to rob the user's browser cookies.
How can you protect against attacks of this kind? Again, obviously, limiting the lifetime of the session identifier and periodically changing the identifier within the same session. We can also change the session name, completely removing the old one and creating a new session, copying all session variables from the old one into it. But this does not affect the essence of the approach, so for simplicity, we restrict ourselves to only the session identifier.
It is clear that the smaller the lifetime of the session identifier, the less time the attacker will have to get and apply cookies to fake the user's request. In the ideal case, for each request a new identifier should be used, which will minimize the possibility of using someone else's session. But we will consider the general case when the regeneration time of the session identifier is arbitrarily set.
(Omit that part of the code that has already been reviewed).
function startSession($isUserActivity=true) {
So, when creating a new session (which occurs when the user successfully logs on), we set the session variable starttime, which stores for us the time of the last generation of the session identifier, equal to the current server time. Further, in each request, we check whether enough time has passed (idLifetime) since the last generation of the identifier, and if it has passed, we generate a new one. Thus, if an attacker who receives an authorized user’s cookie does not have time to use it within the specified ID lifetime, the fake request will be considered by the server as unauthorized, and the attacker will be taken to the login page.
Note: The new session identifier gets into the browser's cookie when the session_regenerate_id () function is called, which sends a new cookie, similar to the session_start () function, so we do not need to update the cookie ourselves.If we want to secure our sessions as much as possible, it is enough to set the identifier's lifetime to one, or else generally remove the function session_regenerate_id () from the brackets and remove all checks, which will lead to the regeneration of the identifier in each request. (I didn’t check the effect of this approach on performance, and I can only say that the session_regenerate_id (true) function performs only 4 actions: generating a new identifier, creating a header with a session cookie, deleting the old one and creating a new session file).
Lyrical digression: If the trojan is so smart that it does not send cookies to the attacker, and organizes sending a pre-prepared fake request immediately upon receipt of the cookie, the method described above will most likely not be able to protect against such an attack, because between receiving a trojan cookie and sending a fake request is almost no difference, and it is likely that at this moment there will be no regeneration of the session identifier.The possibility of simultaneous work in one browser on behalf of several users
The last task I would like to consider is the possibility of simultaneous work of several users in one browser. , , , «».
, , PHP (PHPSESSID). , , , PHPSESSID. , , . , . .
function startSession($isUserActivity=true, $prefix=null) { ... if ( session_id() ) return true;
Now it remains to take care that the calling script passes a unique prefix for each user to the startSession () function. This can be done, for example, by sending a prefix in the GET / POST parameters of each request or through an additional cookie.Conclusion
In conclusion, I will give a complete final code of our functions for working with PHP sessions, which includes all the problems 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 ( $sessionLifetime ) { 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_destroy(); } }
I hope this article will save some time for those who have never really delved into the session mechanism, and will give enough insight into this mechanism for those who are just starting to get familiar with PHP.