📜 ⬆️ ⬇️

Clear cookies

Hi, Habr! In the light of the information on the security of large portals' accounts, which appeared recently, I decided to revise a little cookie authorization in my projects. First of all, he was questioned with a predilection of Google on the topic of ready-made solutions. Nothing sensible was found, although it may be that I do not know how to use the search. After that, I decided to see what they actually write about how to chew cookies correctly. To my surprise, there were no limits when most of the articles from the category of “bad advice”, and what I read more than 5 years ago.
This article is an attempt to rectify the situation.

For many, the following will seem obvious, but I think there are quite a few for whom this information will be useful. In the course of research and reflection on the topic “how to act as mere mortals who have no subdomains,” a bike approach was invented, for which I do not pretend to be authorship, because, as stated above, there is a non-zero likelihood that I do not know how to use search . The examples will use PHP, as this is the most popular language among me, but people with more subtle mental organization should have no problems understanding what is happening. So let's get started.

Cookies will be stored in the best houses of Philadelphia: in a beautiful tin box. That is, during authorization, a cookie is set for a single directory other than document root. Later this cookie is used only when the session is not established or the data (the IP address of the victim user) is not valid. There should be no dynamic content on the cookie verification page.

Now let's take a look at the algorithm in more detail.
')
First we need a SQL table with the following structure:
CREATE TABLE `user_auth_cookies` ( `key` char(32) NOT NULL, `user_id` int(10) unsigned NOT NULL, `logged_in` datetime NOT NULL, PRIMARY KEY (`key`), KEY `user_id` (`user_id`), KEY `logged_in` (`logged_in`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

Some presets, and a small User class:
 $db = new mysqli('localhost', 'test', '', 'test'); if ($db->connect_errno) die('    MySQL: ('.$db->connect_errno.') '.$db->connect_error); //     define('DOMAIN', ($_SERVER['HTTP_HOST'] !== 'localhost' ? $_SERVER['HTTP_HOST'] : false)); //   .  localhost,  false (       ) define('AUTO_AUTH_URL', '/auth/auto'); //      define('AUTH_URL', '/auth'); //     define('AUTH_COOKIE_DURATION', 20); //    ,      define('USE_HTTPS', false); //  HTTPS    (    ) class User { private $_id; public $isGuest = true; public $name = ''; public function __construct() { GLOBAL $db; if (!isset($_SESSION['user']) || $_SESSION['user']['ip'] !== $_SERVER['REMOTE_ADDR']) return; $query = 'SELECT * FROM users WHERE `id` = '.(int)$_SESSION['user']['id']; if (($res = $db->query($query)) !== false && $res->num_rows) { $user = $res->fetch_assoc(); $this->_id = $user['id']; $this->name = $user['name']; $this->isGuest = false; if (isset($_SESSION['last_request'])) { $_POST = $_SESSION['last_request']['data']; unset($_SESSION['last_request']); } } else { unset($_SESSION['user']); } } public function getId() { return $this->_id; } } 

As you can see, two session variables are used, one of them (user) directly for authorization, and the second (last_request) to save the URI from which the authorization request was required, and the POST parameters so that the data of the submitted forms, if any, are not lost.

For authorization, the class Auth is used, containing 4 static methods.
 class Auth { public static function loginRequired() { $_SESSION['last_request'] = array( 'url' => $_SERVER['REQUEST_URI'], 'data' => $_POST ); header('Location: '.AUTO_AUTH_URL); die('...'); } public static function login($login, $password, $remember = false) { GLOBAL $db; $query = "SELECT * FROM users WHERE `login` = '".$db->real_escape_string($login)."';"; if (($res = $db->query($query)) === false || !$res->num_rows) return false; $user = $res->fetch_assoc(); //   ,      if ($user['password'] !== md5($login.md5($password))) return false; if ($remember) { do { $key = md5(mcrypt_create_iv(30)); $query = "SELECT COUNT(*) AS `cnt` FROM user_auth_cookies WHERE `key` = '".$key."';"; $count = 0; if (($res = $db->query($query)) !== false && $res->num_rows) { $row = $res->fetch_assoc(); $count = (int)$row['cnt']; } else die('   .'); } while ($count > 0); $db->query("INSERT INTO user_auth_cookies VALUES ('".$key."', ".$user['id'].", NOW());"); setcookie('key', $key, strtotime('+'.AUTH_COOKIE_DURATION.' days'), AUTO_AUTH_URL, DOMAIN, USE_HTTPS, true); } $_SESSION['user'] = array( 'id' => $user['id'], 'ip' => $_SERVER['REMOTE_ADDR'], ); return true; } public static function loginByCookie() { GLOBAL $db; $location = AUTH_URL; if (isset($_COOKIE['key'])) { $query = "SELECT user_id FROM user_auth_cookies WHERE `key` = '".$db->real_escape_string($_COOKIE['key'])."';"; if (($res = $db->query($query)) !== false && $res->num_rows) { $row = $res->fetch_assoc(); $_SESSION['user'] = array( 'id' => $row['user_id'], 'ip' => $_SERVER['REMOTE_ADDR'] ); $location = '/'; if (isset($_SESSION['last_request'])) $location = $_SESSION['last_request']['url']; } } header('X-Frame-Options: DENY'); //     FRAME/IFRAME header('Location: '.$location); die('...'); } public static function logout() { if (!isset($_SESSION['user'])) return; if (mb_strlen($_SESSION['user']['key']) === 32) $db->query("DELETE FROM user_auth_cookies WHERE `key` = '".$db->real_escape_string($_SESSION['user']['key'])."';"); setcookie('key', '', 0, AUTO_AUTH_URL, DOMAIN, USE_HTTPS, true); unset($_SESSION['user']); header('Location: /'); die('...'); } } 

Auth :: loginRequired () is called if the user is not authorized and goes to the page that requires authorization. The method saves the current URI and POST request parameters to the session variable (saving the POST data is necessary if the user wrote a long angry post and his IP was changed at that moment), and redirects to the automatic cookie authorization page.
In the context of the User class:
 …… $user = new User(); if ($user->isGuest) Auth::loginRequired(); …… 

Auth :: login ($ login, $ password, $ remember = false) is called if the login form is received. The $ login parameter suddenly contains the received login, $ password no less suddenly - the password, $ remember - the flag responsible for setting the cookie.
Usage example:
 …… if (isset($_POST['login']) && Auth::login($_POST['login'], $_POST['password'], !!$_POST['remember_me'])) { $location = '/'; if (isset($_SESSION['last_request'])) $location = $_SESSION['last_request']['url']; header('Location: '.$location); die('...'); } …… 

Auth :: loginByCookie () is invoked on the automatic login page. Let me remind you that in order to avoid unpleasant situations on this page there should be no dynamic output, and you should not load anything from other directories, especially domains. In general, it would not be bad to install a RewriteRule on the script directory, redirecting absolutely all requests for this script. Suppose so:
.htaccess
 <ifModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-U RewriteRule ^.*$ index.php [L,QSA] </ifModule> 

Auth :: logout () is called to "un-login" the user. Clears the cookie and session peremnuyu, removes the key from the base as needed, and redirects to the main one.

Stayed the final touch. It is necessary to periodically (by cron) clear the table of obsolete keys.
 …… $db->query("DELETE FROM user_auth_cookies WHERE `logged_in` < DATE_SUB(NOW(), INTERVAL ".AUTH_COOKIE_DURATION." DAYS);"); …… 

You can also add to the site a button like: “Log me out on all devices”, when clicked, all authorization keys are deleted from the user_auth_cookies table by user_id. This is necessary if the user, for example, forgot to click on "Logout" on another computer, or a mobile device from which he visited your site was stolen from him.
 …… if (isset($_POST['signout_all']) { $db->query("DELETE FROM user_auth_cookies WHERE `user_id` = ".$user->getId()); Auth::logout(); } …… 


For an amateur, you can add a user agent check, or something else like that, but, in my opinion, there is no point in it because if the cookie decides to hijack, then it will be done with the user agent and other similar attributes.

That's all, I wish your liver to remain always fresh and crispy.

Source: https://habr.com/ru/post/210416/


All Articles