Session ID protection in PHP. Pitfalls of using sessions in PHP Passing a value or array using a PHP session

Greetings, dear community.

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

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

The most common example of using sessions is, of course, user authorization. Let's start with the most basic implementation in order to gradually develop it as new tasks arise.

(In order to save space and time, we will limit our examples to only the session functions themselves, instead of building here a full-fledged test application with a beautiful class hierarchy, comprehensive error handling and other good stuff).

Function startSession() ( // If the session has already been started, stop executing 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 if an error occurred. // If you are using a version prior to 5.3.0, perform an additional check for 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 to identify the user in each subsequent request, at the moment of successful login, we need to store the user identifier in a session variable (named userid, for example), which will be available in all subsequent requests within the life of the session. It is also necessary to implement processing the result of our startSession() function. If the function returns FALSE, display the login form in the browser. If the function returned TRUE, and a session variable containing the identifier of the authorized user (in our case - userid), exists - display the page of the authorized user (for more information about error handling, see the addition dated 2013-06-07 in the section on session variables).

So far everything is clear. Questions begin when you need to implement user inactivity control (session timeout), enable multiple users to work simultaneously in one browser, and also protect sessions from unauthorized use. This will be discussed below.

Controlling user inactivity using built-in PHP tools The first question that often arises among developers of various consoles for users is automatic termination of the session in the event of inactivity on the part of the user. There is nothing easier than to do this using the built-in capabilities of PHP. (This option is not particularly reliable or flexible, but we will consider it for 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 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 clarifications. As you know, PHP determines which session needs to be launched by the cookie name 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 browser cookie has expired, it will not be sent in the request, which means PHP will not be able to determine which session to start and will treat this as creating a new session. The PHP settings parameter session.gc_maxlifetime, which is set equal to our user inactivity timeout, sets the lifetime of a PHP session and is controlled by the server. Controlling the session lifetime works as follows (here we consider an example of storing sessions in temporary files as the most common and default option in PHP).

When a new session is created, a file named sess_ is created in the directory set as the directory for storing sessions in the PHP settings parameter session.save_path, where is the session identifier. Next, in each request, at the time of launching 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 time of the last modification of the session file, can determine whether the session is active or 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 applies to all sessions within one server (more precisely, within one main PHP process). In practice, this means that if several sites are running on the server, and each of them has its own user inactivity timeout, then setting this parameter on one of the sites will lead to its setting 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 sessions directory is done using the session.save_path parameter in the php.ini settings file, or by calling the ini_set() function. After this, 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 be valid for its session. We will not consider this case in detail, especially since we have a more flexible option for monitoring user inactivity.

Controlling user inactivity using session variables It would seem that the previous option, for all its simplicity (just a couple of additional lines of code), gives everything we need. But what if not every request can be regarded as the result of user activity? For example, a 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 automatically extending the session lifetime is not correct in this case. But we know that PHP updates the modification time of the session file automatically every time the session_start() function is called, which means that any request will lead to an extension of the session lifetime, and the user inactivity timeout will never occur. In addition, the last note from the previous section about the intricacies of the session.gc_maxlifetime parameter may seem too confusing and difficult to implement for some.

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 ourselves.

Function startSession($isUserActivity=true) ( ​​$sessionLifetime = 300; if (session_id()) return true; // Set the cookie lifetime before closing the browser (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 elapsed since the last user activity // (last request time when the lastactivity session variable was updated) if (isset($_SESSION["lastactivity"]) && $t-$_SESSION["lastactivity"] >= $sessionLifetime) ( // If the time elapsed since the user's last activity, / / is greater than the inactivity timeout, which means the session has expired and you need to terminate the session destroySession(); return false; ) else ( // If the timeout has not yet occurred, // and if the request came as a result of user activity, // update the lastactivity variable with the value of the current one time, // thereby extending the session time by another sessionLifetime seconds if ($isUserActivity) $_SESSION["lastactivity"] = $t; ) ) return true; )

Let's summarize. In each request, we check whether the timeout has been reached since the last user activity until the current moment, and if it has been reached, we destroy the session and interrupt the execution of 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 user’s last activity. All we have to do is 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.

Addition from 2013-06-07 Processing the result of the sessionStart() function

The comments pointed out that returning FALSE does not provide a complete understanding of the cause of the error, and this is absolutely fair. I did not publish detailed error handling here (the length of the article is already quite large), since this is not directly related to the topic of the article. But given the comments, I’ll clarify.

As you can see, the sessionStart function can return FALSE in two cases. Either the session could not be started due to some internal server errors (for example, incorrect session settings in php.ini), or the session lifetime has expired. In the first case, we must redirect the user to a page with an error stating that there are problems on the server and a form for contacting support. In the second case, we must transfer the user to the login form and display a corresponding message in it stating 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 a session on the server still exists, 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 the global PHP settings.

Note: What happens if the browser was closed and the session name cookie was automatically destroyed? The request to the server the next time the browser is opened will not contain the session cookies, and the server will not be able to open the session and check the user's inactivity timeout. For us, this is equivalent to creating a new session and does not affect functionality or security in any way. But a fair question arises - who will then destroy the old session, if until now we have destroyed it after the timeout has expired? Or will it now hang in the sessions directory forever? To clean up old sessions in PHP, there is a mechanism called garbage collection. It runs at the time of the next request to the server and clears all old sessions based on the last modification date of the session files. But the garbage collection mechanism does not start with every request to the server. The frequency (or rather, 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 is the probability of launching the garbage collection mechanism. Thus, in order for the session clearing mechanism to be launched with each request to the server, these parameters must be set to equal values, for example “1”. This approach guarantees a clean session directory, but is obviously too expensive for the server. Therefore, on production systems, the default value of session.gc_divisor is set to 1000, 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 described above, when the browser closes and clears all its cookies, there are still old sessions left in the sessions directory for a while. But this should not worry you, because... as already stated, this does not in any way affect the safety of our mechanism.

Update from 2013-06-07 Preventing scripts from freezing due to session file locking

The comments raised the issue of simultaneously running scripts freezing due to the session file being blocked (the most striking option is 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 are executed. But this is an indirect dependence. 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 regular requests). Each request tries to access the same session file, and if the previous request did not unlock the file, then the subsequent one will hang waiting.

To keep session file locking to a minimum, it is strongly recommended to close the session by calling the session_write_close() function immediately after all actions with session variables have been completed. In practice, this means that you should not store everything in session variables and access them throughout the execution of the script. And if you need to store some working data in session variables, then read them immediately when the session starts, save them in 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 required by the application (if any exist), then close the session using a call to session_write_close() and continue execution of a script, be it a long poll or a regular request.

Protecting sessions from unauthorized use Let's imagine a situation. One of your users gets a Trojan that robs the browser cookies (in which our session is stored) and sends it to the specified email. The attacker obtains the cookie and uses it to spoof 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 was this possible? Obviously, because the name and session identifier are always the same for the entire lifetime of the session, and if you receive this data, you can easily send requests on behalf of another user (of course, within the lifetime of this session). This may not be the most common type of attack, but theoretically it seems quite feasible, especially considering that such a Trojan does not even need administrator rights to rob the 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 name of the session by 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, so for simplicity we will limit ourselves to only the session identifier.

It is clear that the shorter the session ID lifetime, the less time an attacker will have to obtain and use cookies to forge 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 identifier regeneration time is set arbitrarily.

(We will omit the part of the code that has already been discussed).

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

So, when creating a new session (which occurs when the user successfully logs in), we set the session variable starttime, which stores for us the time of the last generation of the session identifier, to a value equal to the current server time. Next, in each request, we check whether enough time (idLifetime) has passed since the last generation of the identifier, and if so, we generate a new one. Thus, if during the set lifetime of the identifier the attacker who received the cookie of the authorized user does not have time to use it, the fake request will be regarded by the server as unauthorized, and the attacker will be taken to the login page.

Note: The new session ID gets into the browser's cookie when the session_regenerate_id() function is called, which sends the new cookie, similar to the session_start() function, so we don't need to update the cookie ourselves.

If we want to make our sessions as secure as possible, it is enough to set the lifetime of the identifier to one or even remove the session_regenerate_id() function from 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 the session cookie, deleting the old one and creating a new session file).

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

Possibility of simultaneous 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 by several users. This feature is especially useful at the testing stage, when you need to emulate the simultaneous work of users, and it is advisable to do this in your favorite browser, rather than using the entire available arsenal or opening several instances of the browser in incognito mode.

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

Function startSession($isUserActivity=true, $prefix=null) ( ... if (session_id()) return true; // If the user prefix is ​​passed in the parameters, // set a unique session name that includes this prefix, // otherwise set 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 all that remains is to make sure 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 provide 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 ($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 delved too deeply into the session mechanism, and give enough insight into this mechanism for those who are just starting to get acquainted with PHP.

Do you need a username and password?

To submit articles online and check the status of submitted articles, you must register and log in to your account.

Checklist for preparing an article for submission

As part of the article submission process, authors must check that their article meets all of the following points; articles may be returned to the authors if they do not meet these requirements.

The article has been prepared in accordance with the requirements

Terms of transfer of copyright

Authors retain copyright of the work and grant the journal first publication rights along with the work, while licensing it under the terms of the Creative Commons Attribution License, which allows others to distribute this work with mandatory attribution to the author of the work and a link to the original publication in this journal.

Privacy Statement

Names and email addresses entered into this magazine's website will be used solely for the purposes designated by this magazine and will not be used for any other purpose or provided to any other person or entity.

Before registering in the system, the user agrees with the policy for the processing and storage of personal data.

Author Payments

1500 characters with spaces: 300.00 (RUB)

Publication of 1 page of manuscript (1500 characters) - 300 rubles. Graphic materials / tables are paid separately - 50 rubles / 1 piece. The author's copy, including shipping within Russia, is paid at the author's request - 400 rubles. Shipping abroad - 800 rubles. The cost of sending a certificate of acceptance of the material for publication is 150 rubles.

Translation of accompanying information (full name, place of work of authors; title; abstract; keywords) into English 0.5 rubles for each character, including spaces.

Attention! Authors (candidates and doctors of science) who, according to elibrary.ru, have 300 or more citations (the share of self-citations should be no more than 30%) are published free of charge. If you are eligible for free publication, when submitting material, in the comments field, indicate a link to your elibrary profile with the number of citations. Shipping costs for the collection are paid separately.

Website security is based 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 or how they navigate from page to page. The session mechanism prevents users from having to enter a password every time they want to perform a new action or go to a new page.

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

After the user is authenticated, the web server provides him with a session ID. This ID is stored in the browser and is substituted whenever authentication is needed. This allows you to avoid repeated 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 ID, is sent to a cookie. But this doesn't always happen. Some users disable cookies in their browsers. In this case, the browser will pass the session ID in the URL.

Here the ID is transmitted in clear text, as opposed to a session via a cookie, when the information is hidden in the HTTP header. The simplest way to protect against this would be to prohibit the transmission of the session identifier through the address bar. This can be done by writing the following in the Apache server .htaccess configuration file:

Php_flag session.use_only_cookies on

Using Encryption If your site must process sensitive information, such as credit card numbers (hello from Sony), you should use SSL3.0 or TSL1.0 encryption. To do this, when setting a cookie, you must specify true for the secure parameter.

If you store the session password in the $_SESSION variable (it’s still better to use sql), then you shouldn’t store it in clear text.

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

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

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

Browser check To prevent the possibility of using a session from another browser (computer), you should enter a check of 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 expiration time of cookies. By default, the session duration 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. Mainly when the number of users is limited and have static IPs. The check can be either based on the 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 IP):

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 hacking cannot be completely avoided. You can only make this hack 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.

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 the 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: