Как безопасно реализовать восстановление пароля в PHP

Эта статья призвана познакомить разработчиков PHP с тем, как правильно реализовать функцию восстановления пароля в PHP. Все мы были когда-то начинающими разработчиками, когда-то мы считали md5 функцию безопасной.

Основная мотивация при написании этой статьи заключается в том, что порывшись в интернете я нашел море рекомендаций на этот счет, но большинство из них не имели ничего общего с безопасностью. В 2017 году 9 из 10 советов в выдаче гугла, да и по сей день довольно много в выдаче небезопасных советов. Хотелось бы немного внести свой вклад и дать людям безопасные варианты.

Теперь, когда с этим разобрались, давайте перейдем к делу.

Оригинал статьи

Содержание
  1. Как (не) стоит реализовать механизм восстановления пароля в PHP
  2. Не храните текстовые пароли в базе данных.
  3. Не используйте общедоступную информацию в качестве маркера восстановления пароля
  4. Не используйте последовательные идентификационные номера в качестве маркеров восстановления пароля.
  5. Не создавайте токены таким образом, который также может быть сгенерирован в автономном режиме кем-то со знанием системы
  6. Генерируйте токены, которые не зависят от пользовательских данных.
  7. Не создавайте свои токены на основе времени,они угадываются.
  8. Не используйте rand, mt_rand или lcg_value в качестве источника случайных чисел для чего-либо связанного с безопасностью.
  9. Используйте random_int или random_bytes для безопасных случайных чисел.
  10. Установите срок службы для ваших жетонов сброса, чем короче, тем лучше. 1 час, вероятно, разумное значение по умолчанию.
  11. Сбросьте жетоны сброса после использования.
  12. Резюмируем

Как (не) стоит реализовать механизм восстановления пароля в PHP

Ну все мы знаем этот функционал. Обычно это делается так: мы запрашиваем почту, которую указал пользователь при регистрации, затем отправляем на почту с ссылкой восстановления пароля. Обычно эта ссылка содержит в себе конкретную информацию, которая позволит приложению узнать, какой пользователь пришел, чтобы восстановить пароль. Затем пользователь указывает новый пароль и авторизуется.

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

Не храните текстовые пароли в базе данных.

Ваш сайт хэширует ваши пароли перед сохранением их в БД? Хорошо, давайте двигаться дальше.

Возвращаясь к отправке по электронной почте пользователям ссылки, чтобы они могли сбросить свой пароль. Предположим, что url-адрес восстановления пароля вашего сайта:

https://your.site / password_recovery.php

Тривиальным решением (опять же, как видно из лучших результатов Google) может быть сделать что-то вроде:

https://your.site/password_recovery.php?user=john@gmail.com

Это не безопасно. Почему? Потому что если злоумышленник знает адрес электронной почты кого-то, он может изменить его пароль. Это наше второе “нет”.

Не используйте общедоступную информацию в качестве маркера восстановления пароля

Вы можете подумать: “Какая еще информация у меня есть от пользователя, которую я могу использовать? Может быть, user_id?”

https://your.site / password_recovery.php?user_id=13

Это тоже небезопасно. Почему? Хотя злоумышленник, скорее всего, не может знать, у какого пользователя он меняет пароль, он может заблокировать людей из их учетных записей. Кроме того, если ваш сайт имеет какую-то функцию администратора, то user_id=1, скорее всего, будет пользователем администратора, и теперь вы попали в мир боли.

Не используйте последовательные идентификационные номера в качестве маркеров восстановления пароля.

На этом этапе вы, вероятно, поняли, что не можете напрямую использовать что-либо из базы данных. Но в попытке обойти это вы думаете: “давайте использовать md5, который должен помешать злоумышленнику угадать пользователя правильно?”. Так что попробуйте что-нибудь вроде:

$token = md5($user["email"]);

https://your.site/password_recovery.php?token=$token

Вы даже можете получить его непосредственно в базе данных, используя функцию md5 в mysql!

Это тоже небезопасно. Почему? Потому что если я могу угадать, как генерируется токен, например: посмотрев на мой токен (или вы опубликовали код в github, или, скажем, я выяснил, какие из топ-10 реализаций google вы копируете и вставляете), то я могу сгенерировать токен для всех. 

Следует проектировать системы, исходя из предположения, что враг сразу же получит полное знакомство с ними

Клод Шеннон

Не ставьте свою безопасность в зависимость от того, что ваш код является неизвестным злоумышленнику. Этого не будет.

Вот несколько вариантов из первой выдачи гугла:

$token = md5 (18247*2567 + $user ["id"]);
$salt = " некая строка !BIG#RANDOM@STRING1337 "
$token = md5 ($salt.$user ["email"])

Все три попадают в 4 пункт. Если я узнаю, как строится ваш хэш, я смогу создать токены восстановления для всех ваших пользователей. Даже если вы уверены, что 4 не относится к вам, не используйте их. Они уязвимы для других атак, которые выходят за рамки этой статьи, но не стесняйтесь нарушать их самостоятельно.

Кроме того, все они проваливают “атаку недовольного (бывшего) коллеги”, то есть: ваш коллега или вы сами можете создавать токены для любого пользователя, которого вы хотите, не взаимодействуя с системой. Это не очень хорошее свойство, чтобы иметь в такой системе.

Не создавайте токены таким образом, который также может быть сгенерирован в автономном режиме кем-то со знанием системы

– Но подождите!” вы можете подумать. “А что, если мы используем шифрование вместо хэширования? Я могу держать ключ вне контроля версий, и мы должны предотвратить эту проблему сверху, верно?”

Пока вы можете это сделать:

$token = encrypt($user ["email"],$some_key);

Это приносит больше проблем, чем решает.

  1. Вам нужно найти способ надежно обращаться с ключом.
  2. Если ключ будет скомпрометирован, и вы захотите его изменить, вы сломаете предыдущие токены.
  3. В зависимости от алгоритма и размера ключа Ключ может быть восстановлен, чему способствует тот факт, что открытый текст и алгоритм известны (см. Не 4).
  4. Если ключ восстановлен (либо с помощью bruteforce, либо из-за другой слабости где-то), то злоумышленник (или ваш коллега, см. Пункт 5 может генерировать токены сброса для любого пользователя в автономном режиме.

Не используйте шифрование, если вы можете избежать этого. Это вызывает больше проблем, чем решает.

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

Генерируйте токены, которые не зависят от пользовательских данных.

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

“Так как же я буду генерировать этот токен?- ты сам себя спрашиваешь. Может быть, использовать что-то, что PHP уже дает нам, например uniqid:

php > echo uniqid();
593aceadf16aa

Это должен быть случайный, уникальный идентификатор, верно???

Изображение для поста
Да, но нет

Выходные данные uniqid генерируются исключительно из текущего времени сервера. Злоумышленник контролирует, когда он запрашивает изменение пароля, поэтому даже если разрешение, которое он использует, составляет до микросекунд, он, вероятно, может сузить диапазон до пары миллисекунд, что делает идентификатор угадываемым за пару тысяч попыток

Не создавайте свои токены на основе времени,они угадываются.

Примечание: хэширование угадываемого токена не добавляет никакой безопасности. Это похоже на то, как когда вы угадываете чей-то пароль, вам все равно, каков хэш его пароля, если вы можете просто догадаться, что пользователь ленив и его пароль буквально “пароль”.

В этот момент Вы, будучи умным парнем/девушкой, можете сказать: “зачем мне использовать uniqid, если я хочу что-то случайное. Я просто использую функции случайных чисел PHP!”

$token = rand (); / / значение между 0 и 2147483647.

Дело в том, что (многие) генераторы случайных чисел являются детерминированными. Если вы знаете несколько выходов, вы действительно можете знать, каким будет следующий. Да, вы все правильно поняли. Нет, я не сумасшедший. Если вы мне не верите, прочтите вот это. Не забудь свою шляпу из фольги, когда вернешься.

И rand (), и mt_rand () уязвимы для такого рода атак. Менее известное значение lcg_value также есть. PHP 7 ввел random_bytes () и random_int (), которые возвращают случайные данные способом, безопасным для такого рода приложений, то есть они являются криптографически защищенными генераторами случайных чисел.

Не используйте rand, mt_rand или lcg_value в качестве источника случайных чисел для чего-либо связанного с безопасностью.

Используйте random_int или random_bytes для безопасных случайных чисел.

Так как же построить случайный и непредсказуемый токен?

Простой способ-это:

$length = 16; // отрегулируйте длину, чтобы соответствовать вашему новому уровню паранойи. 16, вероятно, является нормальным значением по умолчанию и такой же длиной, как md5 (если вы мигрируете из метода, который его использует)

$token = bin2hex(random_bytes($length)); // вывод bin2hex безопасен для url.

Вот и все, мы закончили. У нас есть безопасный токен, который можно использовать для восстановления пароля.

Теперь вы можете пойти дальше и реализовать остальную функциональность. Однако есть пара дополнительных вещей, которые вам нужно иметь в виду, когда вы это делаете.

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

Установите срок службы для ваших жетонов сброса, чем короче, тем лучше. 1 час, вероятно, разумное значение по умолчанию.

Сбросьте жетоны сброса после использования.

Резюмируем

Собрав все это вместе, ваш общий поток мыслей должен выглядеть примерно так:

  • пользователь запрашивает сброс пароля, предоставляя свою электронную почту
  • Найдите пользователя в базе данных, используя адрес электронной почты
  • надежно создайте токен и сохраните его в базе данных вместе со временем его создания. Напр.: $token = bin2hex(random_bytes(16));
  • отправьте электронное письмо со ссылкой на страницу Восстановления пароля и токеном в качестве параметра строки запроса
  • поиск пользователя в базе данных с помощью токена, если он найден и срок действия не истек, запросите у него новый пароль
  • сохраните новый пароль в базе данных
  • удалите используемый токен из базы данных.

Помни, чего не стоит делать:

  1. не храните незашифрованные пароли в базе данных.
  2. не используйте общедоступную информацию в качестве маркера восстановления пароля.
  3. не используйте последовательные идентификационные номера в качестве маркеров восстановления пароля.
  4. сделайте так, чтобы ваша безопасность не зависела от того, что ваш код является секретным.
  5. не генерировать токены таким образом, чтобы они также могли генерироваться в автономном режиме
  6. не используйте шифрование
  7. не генерируйте свои токены на основе времени
  8. не используйте rand, mt_rand или lcg_value в качестве источника случайных чисел для всего, что связано с безопасностью

Помни, как стоит делать:

  1. генерировать токены, которые не зависят от пользовательских данных, и хранить их в базе данных.
  2. используйте random_int или random_bytes для безопасных случайных чисел.
  3. установите срок службы для ваших токенов сброса.
  4. сбросьте токены сброса после использования.
Рейтинг
( 3 оценки, среднее 3 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.