- 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 клавиатуры на своём телефоне. Буквально два нажатия: открыть клавиатуру символов и перейти на вторую страницу. Это уже не говоря о различных альтернативах вроде длинного и очень длинного тире (также известные как –
(–) и —
(—)). Что, опять словарь дополнять? Кстати, если переключить перед этим раскладку клавиатуры – то гборд даже поменяет некоторые символы. То есть мы уже видим проблему множества раскладок. А теперь ещё можно вспомнить про то, что есть другие клавиатуры, и даже другие операционые системы, у которых свои клавиатуры. Ну и на сладенькое...
😏 Эмоджи
😐 тоже
🙁 символы
☹️ .
🤯 БЭЭЭЭМ!
Наш словарь скоро начнёт требовать отдельное распределённое хранилище.
К какому же выводу мы приходим? Константный словарь спецсимволов – это пло что? павильно, хо. Плохо это... Поэтому наш герой-бэкендер достаёт из инвентаря пузырёк витаминов C++ зиплок с регулярками и пишет '/[^wd]/i'
, что в переводе с драугрского означает "Что угодно, кроме букв и цифр. Регистронезависимо."
Удивительно, но с этим даже эмоджи находятся
Не обращайте внимания, что матчей 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
Однако потом меня поправили в комментариях, что у сей буквы в 2017-ом появился её аналог со шифтом. Промариновавшись в фоне где-то часочек эта мысль навела меня на то, что я дурак и забыл про mb
. И тут вскрылось такое...
ẞ – большая, ß – маленькая. Как можно заметить, большая эсцет (ẞ) при её понижении спокойно превращается в маленькую (ß), но вот маленькая эсцет при вознесении её шифтом животворящим внезапно превращается в "SS". То есть применяется её написание до 2017 года.
И при том, каждая из этих "S" – это отдельный символ. То есть если потом опять понизить регистр – вы не получите исходный вариант:
Вот такие вот могут быть подводные камни при работе с регистрами.
/ОТРЕДАКТИРОВАНО
Не будем забывать, что навязанные обязательства на пароль, порой, могут зафейлить просто безупречно подбороустойчивый экземпляр:
Поэтому, на самом деле правильным будет никак не ограничивать пользователя в его паролях. В конце концов, безопасность пользователя – это дело пользователя. Наша же задача как бэкендеров лишь обеспечить безопасность системы (в плане утечек) и хранения (хотя бы захэшировать с солью). Ну и защититься от брутфорса какой-нибудь рекапчей после трёх попыток.
А пароль пользователя должен просто... быть?..
<?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
Нажмите здесь для печати.