Мое веб-приложение использует сеансы для хранения информации о пользователе после входа в систему и для поддержания этой информации при перемещении со страницы на страницу в приложении. В этом конкретном приложении я сохраняю user_id
, first_name
и last_name
человека.
Я хотел бы предложить вариант «Keep Me Logged In» в журнале, который поместит куки-файл на машину пользователя в течение двух недель, что возобновит их сеанс с теми же самыми подробностями, когда они вернутся в приложение.
Каков наилучший подход для этого? Я не хочу хранить их user_id
в cookie, так как кажется, что это облегчит для одного пользователя попытку подделать личность другого пользователя.
Уведомление о безопасности : Основание печенья с хешем MD5 детерминированных данных – плохая идея; лучше использовать случайный токен, полученный из CSPRNG. См . Ответ ircmaxell на этот вопрос для более безопасного подхода.
Обычно я делаю что-то вроде этого:
Конечно, вы можете использовать разные имена файлов 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]]]]]])
Определения
Простой подход
Простое решение:
В приведенном выше примерном примере обобщается весь пример, приведенный на этой странице, но недостатки заключаются в том, что
Лучшее решение
Лучшим решением будет
Пример кода
// 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
Когда злоумышленник собирается красть кулинары, он фокусируется только на конкретном веб-сайте или домене, например. example.com
Но действительно вы можете аутентифицировать пользователя из двух разных доменов ( example.com & fakeaddsite.com ) и сделать его похожим на «Cookie для рекламы »,
Некоторые люди могут задаться вопросом, как вы можете использовать 2 разных файла cookie? Ну его возможно, представьте example.com = localhost
и fakeaddsite.com = 192.168.1.120
. Если вы проверите файлы cookie, это будет выглядеть так
На изображении выше
192.168.1.120
HTTP_REFERER
REMOTE_ADDR
преимущество
Недостаток
улучшение
ajax
Есть две очень интересные статьи, которые я нашел при поиске идеального решения проблемы «помнить меня»:
Я задал здесь один угол этого вопроса, и ответы приведут вас ко всем связанным с маркером тайм-тайм-файлам cookie, которые вам нужны.
В принципе, вы не храните userId в cookie. Вы храните одноразовый токен (огромная строка), который пользователь использует для регистрации своего старого сеанса входа в систему. Затем, чтобы сделать его действительно безопасным, вы запрашиваете пароль для тяжелых операций (например, для изменения самого пароля).
Я бы порекомендовал подход, упомянутый Стефаном (т. Е. Соблюдайте рекомендации в улучшенной передовой практике cookie для зарегистрированных пользователей), а также рекомендуем убедиться, что ваши файлы cookie являются файлами HttpOnly, поэтому они недоступны, возможно, вредоносные, JavaScript.
Создайте хэш, возможно, с секретным только вы знаете, а затем сохраните его в своей БД, чтобы он мог быть связан с пользователем. Должно работать неплохо.
Мое решение такое. Это не 100% пуленепробиваемый, но я думаю, что это спасет вас в большинстве случаев.
Когда пользователь вошел в систему, успешно создайте строку с этой информацией:
$data = (SALT + ":" + hash(User Agent) + ":" + username + ":" + LoginTimestamp + ":"+ SALT)
Шифруйте $data
, задайте тип HttpOnly и установите cookie.
Когда пользователь вернется на ваш сайт, выполните следующие действия:
:
character. Если пользовательские выходы, удалите этот файл 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.
Create a table to store "Remember Me" data in – separate to the user table so that I can log in from multiple devices.
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
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.
On logout, Mark all the "Remember Me" records for that user as expired.
The only vulnerabilities that I can see is;
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)
? 🙂