
Prehistory
Autumn 2015th. About a year and a half ago, when it happened to me to become a participant in the development of a project, where there are substantially more than a couple of dozen users, I finally thought for the first time in my life about the reliability of authorization.
In essence, authorization is what begins the process of interaction between a registered user and the system (for an unregistered user, everything starts with registration, and these two processes, as you already guessed, are very much interrelated). I suddenly realized that in all the projects that I did before, everything is very bad with security.
It is enough to consider the simplest example: someone picked up a password, or a person somehow compromised it. Of course, in our database, hashes are stored using salt, we are even so advanced that we only use HTTPS ... But this does not save us from the human factor.
What does the competent system do in the case of hacking? Blocks an account (quite well - if automatically, for a number of unusual signs in user behavior), further offering means for recovery: an associated phone number, security question, etc. Cell binding is an excellent measure with extremely high reliability. Unfortunately, our project did not have the funds for it, so we had to do without it. Fortunately, blocking can be implemented by placing a person in a special mailbox, or even receive emergency calls to the cellular.
')
And here the traditional method, which is used by many novice developers (found in books, or in articles on the Internet) misfires. The fact is that this method assumes the storage of authorization in sessions. This article does not apply to a specific language, and is valid for any platform, but I will illustrate everything using PHP as an example.
Standard implementation
So, the session in PHP is by default stored in a file. Its id is stored in a cookie (if cookies are disabled, the transfer mechanism through the address bar is activated, if it is not disabled in the configuration). The default lifetime of such a session cookie is until the browser is closed (and usually no one changes it). Therefore, more advanced programmers implement the tick "remember me", or implement its functionality by default, without the ability to disable. What are they doing? Just save in your own cookie ID user. But since it’s easy to store it somehow is too dumb (anyone can put any number and get access to an arbitrary account), they often save the password along with the company’s ID. In the open form.
If someone does not understand why this is bad - imagine that our user is using a very old or very poorly implemented browser. After all, we can not guarantee that the browser securely encrypts cookies? Yes, and all sorts of viruses can be a user on the computer, and in the case of a particularly serious Malvari can not save and encryption - the malware can try to read the values ​​directly from memory when they are decrypted. A person who accidentally finds himself at your computer in your absence can simply copy all the interesting cookies to itself, and in new browsers this process has become even easier. Security holes in the browser can potentially lead to the fact that your cookie will be available to a third-party site (yes, now this is highly unlikely, but what the hell is not joking).
And the most important thing is if we use SSL only during authorization (and on the rest of the pages we decided to disable it for speed gains, or for intermediate caching to work better) ... Then our password is transmitted in clear text all the time. With every request. And the password is rarely changed, that is, it is a kind of token that has a long lifespan. Now imagine that our traffic someone intercepted ...
How to be?
Obviously, something needs to be done about it. Yes, you can immediately run to buy a certificate and connect SSL. But you can do something else before this, and thereby significantly reduce the need for it. In the end, in the same VKontakte SSL was forced only six months ago, and before that somehow lived.
The first thing that comes to mind is to store a hash of it instead of a password. This does not really help if someone intercepts traffic, and does not help when stealing cookies. But in the situation “someone opened the settings and went for a cookie trying to remember the meanings” - it would make the attacker’s life more difficult.
You can go further and use a user agent when creating a hash. Now the hacker will have to use the browser of the same version as ours (and in the case of IE also with the same set of plug-ins of the same versions, which is very unlikely, especially if the hacker did not guess to spy on the user agent).
But if the password is still lost, we are still powerless. Even changing the password through the recovery form or from within the system - the hacker will remain logged in until he closes the browser. After all, the authorization is still based on the session.
You can, of course, in PHP, get a session id when logging in, write it somewhere to the database, and when resetting the password, start all the sessions with the IDs from the database in turn and do session_destroy () ... Possible option, but not necessarily follow it.
So, we formulated the main list of requirements for our system:
1. Be easy to implement
2. It is advisable not to depend on the server platform used.
3. Allow the “hacker” to be thrown out of the system, ending all open sessions, or some of them (based on browser / operating system / login time)
4. See the list of all open sessions in the system, view it
5. Make the attack as difficult as possible if there is no SSL (for example, open Wi-Fi)
6. Do not create inconvenience to legitimate users.
Getting Started
First of all, we will create a table in our database (we will assume that we use a relational DBMS) to store sessions. Our sessions (not to be confused with php shnymi!). The table will have the following fields: id, user_id, ip, user_agent, time. We will keep the session creation time as the time. You can store the last access time, but soon we will see that this is redundant, and we can resort to this only within the framework of denormalization in order to speed up the data sampling. The session will be created at the time of authorization, as well as during registration (we want the user not to have to fill in the login form after registration, and then authorize it immediately). Also create a second table - log, with the same set of fields. There we will add a record at each authorization (via the login form or by cookie).
Now we can store the session id on the client and compare it with the database. At the same time, the server can verify that we have a user agent and IP. If at least one thing does not converge - we consider that the user is not authorized, and direct it to the login page.
But this is somehow not enough - the hacker may know the client's IP (and if he intercepts traffic, then he certainly knows it), the same applies to the user agent (who does not make any problems at all). We would like to store in the cookie not a password, but a certain password derivative tied to the session ... that is, to the user agent and IP. Accordingly, if something became different - access will cease. Therefore, as you may have guessed, it is reasonable to use a hash of these variables, and save it in a cookie. In this case, just looking at the hash, the hacker not only cannot find out the password, but even determine exactly which components were used to create it (and with which modifications).
Let's create one more column in the sessions table - hash. We will use it to store our session hashes.
Convenience issue
But here we face another problem. A client can switch between different IPs (at least between home Wi-Fi and 3G on the road, and also working secure Wi-Fi, as an example). And it would be very inhuman to force him to re-enter the username and password every time you change the IP address. How to be stored in a cookie hash list? When requested by the server to send it? It is possible, but this complicates implementation. In addition, it inflates the size of cookies (while most of the time we may need only one hash, we will keep everything we have ever used), and when cleaning cookies we (as a user) will lose them all, and again we will have to IP addresses enter the password manually.
What if I only bind a hash to a user agent? It is possible, but it will significantly reduce security. We need a compromise approach.
Delegation of authority
Let's try to create a separate module. Let's call it completely artless - security.php, and issue it as a separate script. We will connect it to all closed pages of our project at the very beginning. Inside this file we will analyze certain conditions, and according to the results of its work, set a special flag to 0 or 1. Let this flag be stored in session variables ($ _SESSION array in PHP).
What does this give us? We can cram arbitrary logic into this script, up to the analysis of the user's recent actions and adding him to the IP-list, or blocking his account for a certain period of time. But first, we implement a very basic functionality: we will verify the value of the hash that came from the cookie with what should have happened if the hash was not distorted. The server knows the IP, knows the user agent, knows the password of the current user ... It seems everything is ready!
We divide the initial stage of our external script (this can also be transferred to a separate file, but at the moment it does not matter) into two stages. At first, we look at whether a regular PHP session is established. If not, we immediately transfer the user to the authorization form. But the form checks for the presence of cookies when loading, and if the cookie with the hash of our session exists, and the hash is the same as expected (hash function of the password * and user agent), then set the session variables (first of all user_id), make an entry in the table log, and then we transfer the user back to where he came from (it is reasonable to send the address of the return section directly in the link). So, if there is no PHP session, but there are cookies, and the cookies correspond to the browser - the user is logged in.
NoteThe server, of course, does not know the password. It stores hash and salt to get it. In fact, the cookie token is calculated as md5 ($ password_hash. $ User_agent), that is, this is already a hash of the hash.
Now consider the option when there is a session (after the user is logged in using the password from the form or by cookie, he will also get into this step). And this is where our security.php script comes into play. Here is his code:
<?php
if ($_SESSION['security_check'] == 0) {
$user_id = $_SESSION['user_id'];
$n = (int)mysql_result(mysql_query("SELECT COUNT(*) FROM `sessions` ".
"WHERE `user_id`=".$user_id." AND `ip`='".$_SERVER['REMOTE_ADDR']."' ".
"AND `user_agent`='".mysql_real_escape_string($_SERVER['HTTP_USER_AGENT'])."' ".
"AND `hash`='".preg_replace("/[^0-9a-f]/", "", $_COOKIE['auth'])."'"), 0);
if ($n == 0) {
header("Location: /login?act=logout");
exit();
} else {
$_SESSION['security_check'] = 1;
}
}
//$n = mysql_result(mysql_query("SELECT COUNT(*) FROM `banned` WHERE `ip`='".$_SERVER['REMOTE_ADDR']."'"), 0);
//if ($n > 0) $_SESSION['security_check'] = 0;
?>
— . , , !
. . - — . . IP — , , IP ( ), .
( ) — , , . IP , ( ). , , - — , . , Wi-Fi.
( ? ?) — , .
, . . // ( ). — — , . ( ), , . , security.php. PHP, . , , , .
?
. - SSL. , , ( ). - — . — , . SSL — , . , -, .
, , . — . , , .