- PVSM.RU - https://www.pvsm.ru -

Как правильно проверять сложность пароля пользователя при регистрации

За десятилетия айтишки сложилась практика ограничивать пользователей в сложности их паролей. Сейчас пароль пользователя должен:

  • быть не меньше N символов;

  • && быть не больше M символов (чуть реже встречается такое правило);

  • Содержать хотя бы одну большую букву;

  • Содержать хотя бы одну маленькую букву;

  • Содержать хотя бы одну цифру;

  • Содержать хотя бы один спецсимвол;

  • Иногда можно наткнуться на шедевры, вроде ограничения по количеству символов одинакового типа подряд, из чего рождается бессмертный анекдот про одну грёбаную розовую розу [1]...

Уж где мы только не подстелили соломы для наших нерадивых пользователей, лишь бы они были в безопасности. Ведь и мы, и матанализ, и различные софтинки – все наперебой говорят, что пароль qwerty и пароль W0wS3qur3P/-$$ имеют кардинально разную подбороустойчивость...

Алгоритм везде один и тот же. К слову, обычно проверка на спецсимволы производится "хотя бы однин символ из заранее захардкоженного словаря". Обычно, словарь вида "не буквы и не цифры с обычной клавиатуры английской раскладки". Просто курсор в кавычки константы, глазки на клавиатуру – и погнали составлять наш шикарнейший словарь.

Данный алгоритм настолько распространён, что встречается почти что в каждой первой статье. Примеров подобных статей – тысячи. Есть даже на хабре [2] (в примере кода можно заметить ту самую регулярку).

Однако, всё ли у нас в порядке с нашим алгоритмом?

Что есть спецсимвол?

Дисклеймер: примеры кода будут на пыхе, для простоты забудем про ограничения длины пароля

Типовой шаблон для поиска спецсимволов выглядит в лучшем случае вот так:

$password = 'fF6:';
var_dump(preg_match('/[.:,;?!@#$%^&*_-+=]/', $password));
//true

А что если скобка?

<?php
$password = 'fF6:(';
var_dump(preg_match('/[.:,;?!@#$%^&*_-+=]/', $password));
//false

Ок, не проблема, добавим в словарь ещё и все скобки

<?php
$password = 'fF6:(';
var_dump(preg_match('/[.:,;?!@#$%^&*_-+=()<>{}[]]/', $password));
//true

А как же слэш?

<?php
$password = 'fF6:(/';
var_dump(preg_match('/[.:,;?!@#$%^&*_-+=()<>{}[]]/', $password));
//false

Ок, ладно, сейчас мы сотрём регулярку и заново внимательно перенесём каждый чёртов символ с клавиатуры...

<?php
$password = 'fF6:(/';
var_dump(preg_match('/[.:,;?!@#$%^&*_-+=()<>{}[]'"`/_~|\]/', $password));
//true

И вроде мы собой довольны, однако... "•√π÷׶∆£¢€¥°©®™✓". Вот лишь некоторые символы, которые я слишком легко могу ввести со стандартной GBoard клавиатуры на своём телефоне. Буквально два нажатия: открыть клавиатуру символов и перейти на вторую страницу. Это уже не говоря о различных альтернативах вроде длинного и очень длинного тире (также известные как &ndash; (–) и &mdash; (—)). Что, опять словарь дополнять? Кстати, если переключить перед этим раскладку клавиатуры – то гборд даже поменяет некоторые символы. То есть мы уже видим проблему множества раскладок. А теперь ещё можно вспомнить про то, что есть другие клавиатуры, и даже другие операционые системы, у которых свои клавиатуры. Ну и на сладенькое...

😏 Эмоджи
😐 тоже
🙁 символы
☹️ .
🤯 БЭЭЭЭМ!

Наш словарь скоро начнёт требовать отдельное распределённое хранилище.

К какому же выводу мы приходим? Константный словарь спецсимволов – это пло что? павильно, хо. Плохо это... Поэтому наш герой-бэкендер достаёт из инвентаря пузырёк витаминов C++ зиплок с регулярками и пишет '/[^wd]/i', что в переводе с драугрского означает "Что угодно, кроме букв и цифр. Регистронезависимо."

Удивительно, но с этим даже эмоджи находятся

Как правильно проверять сложность пароля пользователя при регистрации - 1

Не обращайте внимания, что матчей 3. Эмоджи – двухбайтовые символы, поэтому регулярка их воспринимает как "первая половина" и "вторая половина".

Итак, со спецсимволами разобрались. Но всё ли теперь в порядке с нашим алгоритмом?

Отдельный котёл для проверяющих регистр букв

Казалось бы, всё нормально:

<?php
$password = 'Bye, World!';
if (!preg_match('/[A-Z]/', $password)) {
  $errors[] = 'В пароле должна быть хотя бы одна большая буква';
}

if (!preg_match('/[a-z]/', $password)) {
  $errors[] = 'В пароле должна быть хотя бы одна маленькая буква');
}

Однако... Стоит сделать всего лишь вот так

<?php
$password = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'
    . 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';

И наш пароль не пройдёт ни одну проверку на регистры букв. То есть, мы только что напали на проблему: наш алгоритм ищет только буквы англиского алфавита. Это значит, что пароли на французском (é, è, ê), испанском (ñ), немецком (ä, ö, ü), норвежском(ø, Æ) и я могу продолжать ещё очень долго – могут не пройти проверку, если там не будет ни одной буквы английского алфавита.

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

<?php
$password = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'
    . 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';

if ($password === mb_strtolower($password)) {
  $errors[] = 'В пароле должна быть хотя бы одна большая буква';
}

if ($password === mb_strtoupper($password)) {
  $errors[] = 'В пароле должна быть хотя бы одна маленькая буква');
}

Вот так нужно проверять строки на наличие букв конкретного регистра.

Неожиданное заключение

Всё это хорошо, однако...

Существуют языки, в которых просто нет деления на большие и маленькие буквы. Моё исследование под названием "Спроси у ChatGPT" показало, что в мире чуть больше 7000 языков, из них около половины (3500) имеют уникальные алфавиты, и около 20-30% всех языков (1400-2100) не имеют разделения на маленькие и большие буквы. Например, китайский, японский, корейский, иврит, арабский (ChatGPT говорит, что в арабском всего несколько букв имеют разделение на маленькие и большие) и индоевропейские языки. А также почему-то латынь и греческий, с чем я в корне не согласен, ибо тексты на обоих этих языках я видел в большом количестве, и больших букв там ровно каждой маленькой по паре...

ОТРЕДАКТИРОВАНО:

Ранее тут был пассаж про существование букв, у которых нет формы "большая" или "маленькая". В пример я приводил букву эсцет (ß) из немецкого алфавита. И даже проводил некоторые тесты, а потом обнаружил, что я забыл использовать mb_*.

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

<?php
$eszett = 'ß';
var_dump(strtoupper($eszett) === strtolower($eszett)); //true
Как правильно проверять сложность пароля пользователя при регистрации - 2

Однако потом меня поправили в комментариях, что у сей буквы в 2017-ом появился её аналог со шифтом. Промариновавшись в фоне где-то часочек эта мысль навела меня на то, что я дурак и забыл про mb. И тут вскрылось такое...

Как правильно проверять сложность пароля пользователя при регистрации - 3

ẞ – большая, ß – маленькая. Как можно заметить, большая эсцет (ẞ) при её понижении спокойно превращается в маленькую (ß), но вот маленькая эсцет при вознесении её шифтом животворящим внезапно превращается в "SS". То есть применяется её написание до 2017 года.

И при том, каждая из этих "S" – это отдельный символ. То есть если потом опять понизить регистр – вы не получите исходный вариант:

Как правильно проверять сложность пароля пользователя при регистрации - 4

Вот такие вот могут быть подводные камни при работе с регистрами.

/ОТРЕДАКТИРОВАНО

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

Как правильно проверять сложность пароля пользователя при регистрации - 5

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

А пароль пользователя должен просто... быть?..

<?php
//...
if ($password1 === '')) {
  throw new ValidationError('Придумайте пароль');
}

if ($password1 !== $password2)) {
  throw new ValidationError('Пароли не совпадают. Проверьте внимательно и попробуйте ещё раз');
}

//encrypt password and do things

Автор:
Lenald

Источник [3]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/bezopasnost/382551

Ссылки в тексте:

[1] одну грёбаную розовую розу: https://pikabu.ru/story/parol_2658600

[2] на хабре: https://habr.com/ru/post/584020/

[3] Источник: https://habr.com/ru/post/714478/?utm_source=habrahabr&utm_medium=rss&utm_campaign=714478