The Jerusalem PostNew details reveal clashes that stopped beeper op. against Hezbollah days after Oct. 7 - exclusiveESPNBe boring? Lane Kiffin is anything but as he enters a new era at LSUESPN DeportesFC Juárez oficializó a Gustavo Lema como nuevo entrenadorוואלהרוכב אופנוע בן 32 נפצע קשה במהלך רכיבה בתל אביבBollywood HungamaBIG BREAKING: Karan Johar set to bankroll a tentpole divine epic on Radha Krishna; signs Sachin Ravi as the directorThe South AfricanConfirmed Transfer News: Orlando Pirates sign Bafana wingerIl Fatto QuotidianoCrimini a Gaza: ecco “Shachpatz”, la app per i militari israeliani all’estero che vogliono sfuggire agli arrestiNotJustOkEvery detail of the Davido and Erigga clash on social mediaIl Sole 24 OrePonte sullo Stretto, entro settembre via all’istruttoria per il Cipess. «Apertura nel 2034»BillboardHow Historic Was Olivia Rodrigo’s Achievement With Her Daisy Chain Fields Festival? Greatest Pop Stars PodcastNTVArif Ahmet Denizolgun'un şüpheli ölümü. Doktoru gözaltına alındıWirtualna PolskaZatrzymani ws. Zondacrypto. Prokuratura postawiła zarzuty
The Daily Newsstand · Free, Always
Thursday, September 3, 2026

Пользователь придумал пароль из 100 символов. От 28 из них ваша проверка не зависит

Translate

Тикет пришёл от разработчика, который писал импорт учёток из старой системы. Формулировка была такая: «кажется, у нас ломается верификация, но ломается она в плюс — подходят пароли, которых не было».

Ломалось не у него. Так работает bcrypt, и работает уже двадцать семь лет.

Что происходит с длинным паролем

bcrypt читает первые 72 байта пароля. Всё, что дальше, в вычислении не участвует. Не отбрасывается с ошибкой, не приводит к другому хешу — просто не читается.

Проще всего это увидеть на PHP, где password_hash — стандартный способ из документации (алгоритм задан явным PASSWORD_BCRYPT, чтобы он не поехал при смене умолчания):

$ php -r '
$p72  = str_repeat("A", 72);
$p100 = str_repeat("A", 100);
$h = password_hash($p72, PASSWORD_BCRYPT);
echo "hash: $h\n";
echo "72  : ".var_export(password_verify($p72,  $h), true)."\n";
echo "100 : ".var_export(password_verify($p100, $h), true)."\n";
'
hash: $2y$12$PbZr8al6MKXZcBYUsVUlte5tWqUdDUsDEr5yS3SWMmGieM/sQodoC
72  : true
100 : true

Хеш посчитан от 72 байт. Пароль из 100 символов к нему подошёл. Предупреждения не было ни при хешировании, ни при проверке.

Хвост может быть и другим по содержанию, не только длиннее:

$ php -r '
$h = password_hash(str_repeat("A",72), PASSWORD_BCRYPT);
$a = str_repeat("A",72)."xxxxx";
$b = str_repeat("A",72)."СОВСЕМ ДРУГОЕ";
echo "A: len=".strlen($a)." verify=".var_export(password_verify($a,$h),true)."\n";
echo "B: len=".strlen($b)." verify=".var_export(password_verify($b,$h),true)."\n";
'
A: len=77 verify=true
B: len=97 verify=true

С точки зрения bcrypt это один и тот же пароль.

Почему 72

bcrypt построен на блочном шифре Blowfish, у которого ключ разворачивается в таблицу подключей. Таблица P занимает 18 подключей по 4 байта — это ровно 72 байта. Сам Blowfish умеет и более длинные ключи, он их просто циклически повторяет; но Провос и Мазьер зафиксировали размер таблицы как жёсткую границу длины пароля, и всё, что за неё выходит, до функции не доходит.

Ограничение попало в статью 1999 года, в которой bcrypt и был предложен, и с тех пор не менялось: менять его — значит получить другой алгоритм и несовместимые хеши.

Байты, а не символы

72 — это байты. Латиница в UTF-8 — один байт на символ, и до лимита действительно надо дописать 72 знака. Кириллица — два байта на символ:

$ php -r '
foreach ([35,36,37] as $n) {
  $s = str_repeat("я", $n);
  printf("%d символов = %d байт%s\n", $n, strlen($s), strlen($s) > 72 ? "  <- за границей" : "");
}'
35 символов = 70 байт
36 символов = 72 байт
37 символов = 74 байт  <- за границей

Тридцать шесть символов — и лимит выбран целиком. Тридцать седьмой уже не участвует:

$ php -r '
$p36 = str_repeat("я",36);
$h = password_hash($p36, PASSWORD_BCRYPT);
echo "36 симв (72 байта): ".var_export(password_verify($p36,$h),true)."\n";
echo "36 симв + хвост   : ".var_export(password_verify($p36."ДРУГОЕ",$h),true)."\n";'
36 симв (72 байта): true
36 симв + хвост   : true

Это перестаёт быть экзотикой ровно в тот момент, когда вы начинаете рассказывать пользователям про парольные фразы. Фраза на кириллице из четырёх слов — это 30–45 символов, то есть 60–90 байт. Начиная с тридцать седьмого символа хвост перестаёт учитываться, и два разных пароля становятся одним:

$ php -r '
$base = "КорректнаяЛошадьБатарейкаСкрепкаДлинныйПароль";
$p1 = $base."ХвостОдин";
$p2 = $base."СовсемДругойХвост";
echo "p1 байт: ".strlen($p1)."   p2 байт: ".strlen($p2)."\n";
$h = password_hash($p1, PASSWORD_BCRYPT);
echo "verify(p2, hash(p1)) = ".var_export(password_verify($p2,$h),true)."\n";
'
p1 байт: 108   p2 байт: 124
verify(p2, hash(p1)) = true

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

С эмодзи ещё теснее: одиночная кодовая точка — четыре байта, и таких знаков влезает восемнадцать. Составные, которые собираются из нескольких точек через соединители и модификаторы тона, съедают по 8–25 байт за один видимый символ, и до потолка их доходит от трёх до восьми.

Насколько это опасно

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

Проблема не в стойкости, а в том, что система ведёт себя не так, как показывает. Проявляется это в трёх местах, и все три — про эксплуатацию.

Менеджеры паролей. Сгенерированная строка на 100 символов сохраняется у пользователя целиком, а проверяется по первым 72. Пока всё в одной системе — незаметно. Появляется второй сервис с другой реализацией, которая режет длину по-своему или отвергает такой пароль целиком, — и пользователь получает «неверный пароль» при правильном пароле, а вы получаете тикет, который никто не может воспроизвести.

Миграция и сравнение реализаций. Одна библиотека молча берёт первые 72 байта, другая бросает исключение, а среди обёрток встречаются такие, которые обрезают строку до 72 символов ещё до кодирования в UTF-8, — и на кириллице дают третий результат. При переносе базы это всплывает как «часть пользователей не может войти» без всякой закономерности, потому что закономерность в кодировке их паролей.

Совпадение там, где его не ждут. Общий 72-байтовый префикс делает пароли неразличимыми. Сценарий надуманный ровно до тех пор, пока в компании не появляется корпоративный шаблон вроде длинного постоянного префикса плюс что-то в конце.

Библиотеки перестали молчать

За последние несколько лет часть реализаций перешла от тихой обрезки к явному отказу. Python-пакет bcrypt, начиная с ветки 4.x, не хеширует и не проверяет длинный пароль вообще:

$ python3 -c "
import bcrypt
h = bcrypt.hashpw(b'A'*72, bcrypt.gensalt(10))
print('72 ok:', bcrypt.checkpw(b'A'*72, h))
print('73 ok:', bcrypt.checkpw(b'A'*73, h))
"
72 ok: True
Traceback (most recent call last):
ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])

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

Так что перед обновлением стоит посмотреть, есть ли у вас вообще ограничение длины на форме. Обычно выясняется, что на форме ограничения нет, а в библиотеке хеширования — уже есть.

Как чинят

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

Или предварительно хешировать. Пароль прогоняется через SHA-256, результат кодируется в base64, и в bcrypt уходит эта строка. Длина входа перестаёт иметь значение: на bcrypt всегда приходит ровно 44 байта, сколько бы ни ввёл пользователь.

Ключевое слово тут — base64, и вот почему. Сырой двоичный вывод SHA-256 подавать в bcrypt нельзя: в исторических реализациях на C пароль — это строка, оканчивающаяся нулевым байтом, и всё после первого нуля отбрасывалось. Нулевой байт в 32 случайных байтах — событие вовсе не редкое:

$ python3 -c "print('%.1f%%' % ((1-(255/256)**32)*100))"
11.8%

Примерно каждый восьмой пароль превратился бы в обрубок, а часть из них — в совсем короткий. Base64 даёт печатные символы, и вопрос снимается.

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

Хешировать дважды одним bcrypt («bcrypt поверх bcrypt») смысла не имеет: результат — печатная строка длиной 60 байт, до лимита не дотягивает, но и проблему не решает, а стоимость удваивает.

Или взять алгоритм, у которого такого ограничения нет. Argon2id и scrypt принимают вход любой длины. Если система новая — вопрос на этом закрывается. Если система старая — переезд делается прозрачно: при следующем успешном входе пароль уже проверен, в этот момент он есть в открытом виде, и его можно перехешировать новым алгоритмом, записав рядом маркер версии. Через несколько месяцев остаются те, кто не заходил, и им назначают принудительную смену пароля.

Само по себе долгое хранение bcrypt-хешей — не авария. Алгоритм не сломан, и претензия к нему ровно одна — вот эти 72 байта.

Что посмотреть у себя

Три проверки, каждая отвечает на свой вопрос.

Есть ли на форме регистрации верхняя граница длины и совпадает ли она с тем, что валидирует сервер. Обычно они не совпадают.

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

Все ли места, где вызывается хеширование, ограничивают длину. Их почти всегда больше одного: регистрация, смена пароля, сброс, импорт, сидинг тестовых данных, админка. Если хоть одно не ограничивает, останется путь, по которому в базу попадёт хеш обрезанного пароля.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.