«Keep Me Logged In» – лучший подход

Мое веб-приложение использует сеансы для хранения информации о пользователе после входа в систему и для поддержания этой информации при перемещении со страницы на страницу в приложении. В этом конкретном приложении я сохраняю user_id , first_name и last_name человека.

Я хотел бы предложить вариант «Keep Me Logged In» в журнале, который поместит куки-файл на машину пользователя в течение двух недель, что возобновит их сеанс с теми же самыми подробностями, когда они вернутся в приложение.

Каков наилучший подход для этого? Я не хочу хранить их user_id в cookie, так как кажется, что это облегчит для одного пользователя попытку подделать личность другого пользователя.

Уведомление о безопасности : Основание печенья с хешем MD5 детерминированных данных – плохая идея; лучше использовать случайный токен, полученный из CSPRNG. См . Ответ ircmaxell на этот вопрос для более безопасного подхода.

Обычно я делаю что-то вроде этого:

  1. Пользователь входит в систему с 'keep me logged in'
  2. Создать сеанс
  3. Создайте файл cookie под названием SOMETHING, содержащий: md5 (соль + имя пользователя + ip + соль) и файл cookie с именем somethingElse, содержащий идентификатор
  4. Хранить файл cookie в базе данных
  5. Пользователь делает вещи и оставляет —-
  6. Пользователь возвращает, проверяет наличие somethingElse cookie, если он существует, получает старый хэш из базы данных для этого пользователя, проверяя совпадение содержимого файла cookie SOMETHING с хешем из базы данных, которое также должно совпадать с недавно вычисленным хешем (для ip), таким образом: cookieHash == databaseHash == md5 (соль + имя пользователя + ip + соль), если они это делают, goto 2, если они не имеют 1

Конечно, вы можете использовать разные имена файлов cookie и т. Д. Также вы можете немного изменить содержимое файла cookie, просто убедитесь, что его не легко создать. Например, вы можете создать user_salt, когда пользователь будет создан, а также поместить его в файл cookie.

Также вы можете использовать sha1 вместо md5 (или почти любой алгоритм)

Хорошо, позвольте мне прямо сказать: если вы ставите данные пользователя или что-либо, полученное из пользовательских данных, в cookie для этой цели, вы делаете что-то неправильно.

Там. Я сказал это. Теперь мы можем перейти к фактическому ответу.

Что случилось с хэшированием пользовательских данных, спросите вы? Ну, это сводится к поверхности воздействия и безопасности через неясность.

Представьте себе, что вы нападавший. Вы видите набор криптографических файлов cookie для запоминания во время сеанса. Его ширина составляет 32 символа. Gee. Это может быть MD5 …

Давайте также предположим, что они знают алгоритм, который вы использовали. Например:

 md5(salt+username+ip+salt) 

Теперь все, что нужно злоумышленнику, это грубая сила «соли» (которая на самом деле не соль, но больше об этом позже), и теперь он может генерировать все фальшивые токены, которые он хочет, с любым именем пользователя для своего IP-адреса! Но грубая заставлять соль трудно, не так ли? Абсолютно. Но современные графические процессоры чрезвычайно хороши в этом. И если вы не используете в нем достаточную случайность (сделайте ее достаточно большой), она быстро упадет, а вместе с ней и ключи к вашему замку.

Короче говоря, единственное, что вас защищает, это соль, которая на самом деле не защищает вас так сильно, как вы думаете.

Но ждать!

Все это предполагалось, что злоумышленник знает алгоритм! Если это секретно и запутанно, тогда вы в безопасности, не так ли? НЕПРАВИЛЬНО . Эта линия мышления имеет имя: Security Through Obscurity , на которое НИКОГДА не следует полагаться.

Лучший способ

Лучше всего никогда не позволять информации пользователя покидать сервер, кроме идентификатора.

Когда пользователь входит в систему, генерирует большой (от 128 до 256 бит) случайный токен. Добавьте это в таблицу базы данных, которая сопоставляет токен с идентификатором пользователя, а затем отправит его клиенту в файл cookie.

Что делать, если злоумышленник угадывает случайный токен другого пользователя?

Ну, давайте сделаем кое-какую математику. Мы генерируем 128-битный случайный токен. Это означает, что есть:

 possibilities = 2^128 possibilities = 3.4 * 10^38 

Теперь, чтобы показать, насколько абсурдно большой этот номер, давайте представим себе каждый сервер в Интернете (скажем, 50 000 000 сегодня), пытающийся перевести это число со скоростью 1 000 000 000 в секунду. На самом деле ваши серверы будут таять под такой нагрузкой, но давайте разберемся.

 guesses_per_second = servers * guesses guesses_per_second = 50,000,000 * 1,000,000,000 guesses_per_second = 50,000,000,000,000,000 

Так что 50 квадриллионов догадок в секунду. Это быстро! Правильно?

 time_to_guess = possibilities / guesses_per_second time_to_guess = 3.4e38 / 50,000,000,000,000,000 time_to_guess = 6,800,000,000,000,000,000,000 

Итак, 6,8 сек. Секунд …

Давайте попробуем довести это до более дружественных чисел.

 215,626,585,489,599 years 

Или еще лучше:

 47917 times the age of the universe 

Да, это 47917 раз больше возраста вселенной …

В принципе, он не будет взломан.

Итак, подведем итог:

Лучшим подходом, который я рекомендую, является сохранение файла cookie с тремя частями.

 function onLogin($user) { $token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit storeTokenForUser($user, $token); $cookie = $user . ':' . $token; $mac = hash_hmac('sha256', $cookie, SECRET_KEY); $cookie .= ':' . $mac; setcookie('rememberme', $cookie); } 

Затем, чтобы проверить:

 function rememberMe() { $cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : ''; if ($cookie) { list ($user, $token, $mac) = explode(':', $cookie); if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) { return false; } $usertoken = fetchTokenByUserName($user); if (hash_equals($usertoken, $token)) { logUserIn($user); } } } 

Примечание. Не используйте маркер или комбинацию пользователя и токена для поиска записи в вашей базе данных. Всегда обязательно загружайте запись на основе пользователя и используйте функцию сравнения по времени, чтобы впоследствии сравнить извлеченный токен. Подробнее о тайм-атаках .

Теперь очень важно, чтобы SECRET_KEY был криптографическим секретом (сгенерированным чем-то вроде /dev/urandom и / или производным от входа с высокой энтропией). Кроме того, GenerateRandomToken() должен быть сильным случайным источником ( mt_rand() не достаточно силен. Используйте библиотеку, такую ​​как RandomLib или random_compat , или mcrypt_create_iv() с DEV_URANDOM ) …

Функция hash_equals() предназначена для предотвращения временных атак . Если вы используете версию PHP ниже PHP 5.6, функция hash_equals() не поддерживается. В этом случае вы можете заменить hash_equals() функцией timingSafeCompare:

 /** * A timing safe equals comparison * * To prevent leaking length information, it is important * that user input is always used as the second parameter. * * @param string $safe The internal (safe) value to be checked * @param string $user The user submitted (unsafe) value * * @return boolean True if the two strings are identical. */ function timingSafeCompare($safe, $user) { if (function_exists('hash_equals')) { return hash_equals($safe, $user); // PHP 5.6 } // Prevent issues if string length is 0 $safe .= chr(0); $user .= chr(0); // mbstring.func_overload can make strlen() return invalid numbers // when operating on raw binary strings; force an 8bit charset here: if (function_exists('mb_strlen')) { $safeLen = mb_strlen($safe, '8bit'); $userLen = mb_strlen($user, '8bit'); } else { $safeLen = strlen($safe); $userLen = strlen($user); } // Set the result to the difference between the lengths $result = $safeLen - $userLen; // Note that we ALWAYS iterate over the user-supplied length // This is to prevent leaking length information for ($i = 0; $i < $userLen; $i++) { // Using % here is a trick to prevent notices // It's safe, since if the lengths are different // $result is already non-0 $result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i])); } // They are only identical strings if $result is exactly 0... return $result === 0; } 

Введение

Ваш заголовок «Keep Me Logged In» – лучший подход затрудняет мне знать, с чего начать, потому что, если вы ищете лучший подход, вам нужно будет рассмотреть следующее:

  • Идентификация
  • Безопасность

Печенье

Cookies уязвимы. Между общими уязвимостями браузера cookie-кражи и атаками межсайтового скриптинга мы должны признать, что файлы cookie небезопасны. Чтобы повысить безопасность, вы должны заметить, что у php setcookies есть дополнительные функции, такие как

bool setcookie (string $ name [, string $ value [, int $ expire = 0 [, string $ path [, string $ domain [, bool $ secure = false [, bool $ httponly = false]]]]]])

  • безопасный (с использованием HTTPS-соединения)
  • httponly (Уменьшить кражу личных данных с помощью атаки XSS)

Определения

  • Токен (непредсказуемая случайная строка из n длины, например. / Dev / urandom)
  • Ссылка (непредсказуемая случайная строка из n длины, например. / Dev / urandom)
  • Подпись (генерировать значение хеш-ключа с использованием метода HMAC)

Простой подход

Простое решение:

  • Пользователь вошел в систему с помощью Запомнить меня
  • Войти Cookie, выпущенный с помощью токена и подписи
  • Когда возвращается, Подпись проверяется
  • Если подпись не работает, тогда имя пользователя и токен просматриваются в базе данных
  • если не действительны .. вернуться на страницу входа
  • Если действительный автоматический вход

В приведенном выше примерном примере обобщается весь пример, приведенный на этой странице, но недостатки заключаются в том, что

  • Невозможно узнать, были ли похищены куки
  • Атакующим могут быть операции с доступом, такие как смена пароля или данные, такие как личная и выпечка информации и т. Д.
  • Скомпрометированный файл cookie будет по-прежнему действителен для продолжительности жизни файлов cookie

Лучшее решение

Лучшим решением будет

  • Пользователь вошел в систему и помнит, что меня выбрали
  • Создать токен и подпись и сохранить в cookie
  • Токены являются случайными и действительны только для одиночной аутентификации
  • Символы заменяются при каждом посещении сайта
  • Когда незарегистрированный пользователь посещает сайт, подписи, токены и имя пользователя проверяются
  • Помните, что логин должен иметь ограниченный доступ и не допускать изменения пароля, личной информации и т. Д.

Пример кода

 // Set privateKey // This should be saved securely $key = 'fc4d57ed55a78de1a7b31e711866ef5a2848442349f52cd470008f6d30d47282'; $key = pack("H*", $key); // They key is used in binary form // Am Using Memecahe as Sample Database $db = new Memcache(); $db->addserver("127.0.0.1"); try { // Start Remember Me $rememberMe = new RememberMe($key); $rememberMe->setDB($db); // set example database // Check if remember me is present if ($data = $rememberMe->auth()) { printf("Returning User %s\n", $data['user']); // Limit Acces Level // Disable Change of password and private information etc } else { // Sample user $user = "baba"; // Do normal login $rememberMe->remember($user); printf("New Account %s\n", $user); } } catch (Exception $e) { printf("#Error %s\n", $e->getMessage()); } 

Используемый класс

 class RememberMe { private $key = null; private $db; function __construct($privatekey) { $this->key = $privatekey; } public function setDB($db) { $this->db = $db; } public function auth() { // Check if remeber me cookie is present if (! isset($_COOKIE["auto"]) || empty($_COOKIE["auto"])) { return false; } // Decode cookie value if (! $cookie = @json_decode($_COOKIE["auto"], true)) { return false; } // Check all parameters if (! (isset($cookie['user']) || isset($cookie['token']) || isset($cookie['signature']))) { return false; } $var = $cookie['user'] . $cookie['token']; // Check Signature if (! $this->verify($var, $cookie['signature'])) { throw new Exception("Cokies has been tampared with"); } // Check Database $info = $this->db->get($cookie['user']); if (! $info) { return false; // User must have deleted accout } // Check User Data if (! $info = json_decode($info, true)) { throw new Exception("User Data corrupted"); } // Verify Token if ($info['token'] !== $cookie['token']) { throw new Exception("System Hijacked or User use another browser"); } /** * Important * To make sure the cookie is always change * reset the Token information */ $this->remember($info['user']); return $info; } public function remember($user) { $cookie = [ "user" => $user, "token" => $this->getRand(64), "signature" => null ]; $cookie['signature'] = $this->hash($cookie['user'] . $cookie['token']); $encoded = json_encode($cookie); // Add User to database $this->db->set($user, $encoded); /** * Set Cookies * In production enviroment Use * setcookie("auto", $encoded, time() + $expiration, "/~root/", * "example.com", 1, 1); */ setcookie("auto", $encoded); // Sample } public function verify($data, $hash) { $rand = substr($hash, 0, 4); return $this->hash($data, $rand) === $hash; } private function hash($value, $rand = null) { $rand = $rand === null ? $this->getRand(4) : $rand; return $rand . bin2hex(hash_hmac('sha256', $value . $rand, $this->key, true)); } private function getRand($length) { switch (true) { case function_exists("mcrypt_create_iv") : $r = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); break; case function_exists("openssl_random_pseudo_bytes") : $r = openssl_random_pseudo_bytes($length); break; case is_readable('/dev/urandom') : // deceze $r = file_get_contents('/dev/urandom', false, null, 0, $length); break; default : $i = 0; $r = ""; while($i ++ < $length) { $r .= chr(mt_rand(0, 255)); } break; } return substr(bin2hex($r), 0, $length); } } 

Тестирование в Firefox и Chrome

введите описание изображения здесь

преимущество

  • Лучшая безопасность
  • Ограниченный доступ для злоумышленника
  • Когда cookie украден, он действителен только для одного доступа
  • Когда следующий исходный пользователь получает доступ к сайту, вы можете автоматически обнаруживать и уведомлять пользователя о краже

Недостаток

  • Не поддерживает постоянное соединение через несколько браузеров (Mobile & Web)
  • Файл cookie все равно может быть украден, потому что пользователь получает уведомление только после следующего входа.

Быстрая починка

  • Внедрение системы утверждения для каждой системы, которая должна иметь постоянное соединение
  • Использовать несколько файлов cookie для аутентификации

Множественный подход к использованию файлов cookie

Когда злоумышленник собирается красть кулинары, он фокусируется только на конкретном веб-сайте или домене, например. example.com

Но действительно вы можете аутентифицировать пользователя из двух разных доменов ( example.com & fakeaddsite.com ) и сделать его похожим на «Cookie для рекламы »,

  • Пользователь зашел на example.com, чтобы запомнить меня
  • Сохранять имя пользователя, токен, ссылку в cookie
  • Храните имя пользователя, токен, ссылку в базе данных, например. Memcache
  • Отправить refrence id через get и iframe на fakeaddsite.com
  • fakeaddsite.com использует ссылку для извлечения пользователя и токена из базы данных
  • fakeaddsite.com сохраняет подпись
  • Когда пользователь возвращает информацию подписи fetch с iframe из fakeaddsite.com
  • Объедините данные и выполните проверку
  • ….. вы знаете, что остальные

Некоторые люди могут задаться вопросом, как вы можете использовать 2 разных файла cookie? Ну его возможно, представьте example.com = localhost и fakeaddsite.com = 192.168.1.120 . Если вы проверите файлы cookie, это будет выглядеть так

введите описание изображения здесь

На изображении выше

  • Текущее посещение сайта – localhost
  • Он также содержит файлы cookie, установленные с 192.168.1.120

192.168.1.120

  • Принимает только определенный HTTP_REFERER
  • Принимает только соединение из указанного REMOTE_ADDR
  • Нет JavaScript, нет содержимого, но состоит из ничего, кроме знака и добавления или извлечения из файла cookie.

преимущество

  • 99% процента времени, когда вы обманули атакующего
  • Вы можете легко заблокировать учетную запись в первой попытке злоумышленника
  • Атаку можно предотвратить даже до следующего входа, как и другие методы

Недостаток

  • Несколько запросов на сервер только для одного входа

улучшение

  • Выполнено использование iframe ajax

Есть две очень интересные статьи, которые я нашел при поиске идеального решения проблемы «помнить меня»:

  • Персистентная версия Cookie для входа в систему
  • Улучшенная постоянная версия cookie для входа в систему

Я задал здесь один угол этого вопроса, и ответы приведут вас ко всем связанным с маркером тайм-тайм-файлам cookie, которые вам нужны.

В принципе, вы не храните userId в cookie. Вы храните одноразовый токен (огромная строка), который пользователь использует для регистрации своего старого сеанса входа в систему. Затем, чтобы сделать его действительно безопасным, вы запрашиваете пароль для тяжелых операций (например, для изменения самого пароля).

Я бы порекомендовал подход, упомянутый Стефаном (т. Е. Соблюдайте рекомендации в улучшенной передовой практике cookie для зарегистрированных пользователей), а также рекомендуем убедиться, что ваши файлы cookie являются файлами HttpOnly, поэтому они недоступны, возможно, вредоносные, JavaScript.

Создайте хэш, возможно, с секретным только вы знаете, а затем сохраните его в своей БД, чтобы он мог быть связан с пользователем. Должно работать неплохо.

Мое решение такое. Это не 100% пуленепробиваемый, но я думаю, что это спасет вас в большинстве случаев.

Когда пользователь вошел в систему, успешно создайте строку с этой информацией:

 $data = (SALT + ":" + hash(User Agent) + ":" + username + ":" + LoginTimestamp + ":"+ SALT) 

Шифруйте $data , задайте тип HttpOnly и установите cookie.

Когда пользователь вернется на ваш сайт, выполните следующие действия:

  1. Получите данные cookie. Удалите опасные символы внутри файла cookie. Разверните его с помощью : character.
  2. Проверьте правильность. Если cookie старше X дней, переадресуйте пользователя на страницу входа в систему.
  3. Если cookie не старый; Получите последнее время смены пароля из базы данных. Если пароль изменен после последнего входа пользователя в систему, перенаправите пользователя на страницу входа.
  4. Если пропуск не был изменен в последнее время; Получить текущий агент браузера. Проверьте (currentUserAgentHash == cookieUserAgentHash). IF-агенты одинаково переходят к следующему шагу, иначе перенаправляются на страницу входа.
  5. Если все шаги успешно пройдены, авторизуйте имя пользователя.

Если пользовательские выходы, удалите этот файл cookie. Create new cookie if user re-logins.

I don't understand the concept of storing encrypted stuff in a cookie when it is the encrypted version of it that you need to do your hacking. If I'm missing something, please comment.

I am thinking about taking this approach to 'Remember Me'. If you can see any issues, please comment.

  1. Create a table to store "Remember Me" data in – separate to the user table so that I can log in from multiple devices.

  2. On successful login (with Remember Me ticked):

    a) Generate a unique random string to be used as the UserID on this machine: bigUserID

    b) Generate a unique random string: bigKey

    c) Store a cookie: bigUserID:bigKey

    d) In the "Remember Me" table, add a record with: UserID, IP Address, bigUserID, bigKey

  3. If trying to access something that requires login:

    a) Check for the cookie and search for bigUserID & bigKey with a matching IP address

    b) If you find it, Log the person in but set a flag in the user table "soft login" so that for any dangerous operations, you can prompt for a full login.

  4. On logout, Mark all the "Remember Me" records for that user as expired.

The only vulnerabilities that I can see is;

  • you could get hold of someone's laptop and spoof their IP address with the cookie.
  • you could spoof a different IP address each time and guess the whole thing – but with two big string to match, that would be…doing a similar calculation to above…I have no idea…huge odds?

I read all the answers and still found it difficult to extract what I was supposed to do. If a picture is worth 1k words I hope this helps others implement a secure persistent storage based on Barry Jaspan's Improved Persistent Login Cookie Best Practice

введите описание изображения здесь

If you have questions, feedback, or suggestions, I will try to update the diagram to reflect for the newbie trying to implement a secure persistent login.

Implementing a "Keep Me Logged In" feature means you need to define exactly what that will mean to the user. In the simplest case, I would use that to mean the session has a much longer timeout: 2 days (say) instead of 2 hours. To do that, you will need your own session storage, probably in a database, so you can set custom expiry times for the session data. Then you need to make sure you set a cookie that will stick around for a few days (or longer), rather than expire when they close the browser.

I can hear you asking "why 2 days? why not 2 weeks?". This is because using a session in PHP will automatically push the expiry back. This is because a session's expiry in PHP is actually an idle timeout.

Now, having said that, I'd probably implement a harder timeout value that I store in the session itself, and out at 2 weeks or so, and add code to see that and to forcibly invalidate the session. Or at least to log them out. This will mean that the user will be asked to login periodically. Yahoo! does this.

Use some kind of "cookie" hash, the username + md5($ip, $username, $password) ? That would be my suggestion.

And if(md5($_SERVER['REMOTE_ADDR'], $username, $password) = $cookiehash) ? 🙂