В ноябре прошлого года широко обсуждалась история компании Okta, когда из-за неочевидной особенности Bcrypt оказалось возможным «сломать» часть процесса аутентификации. Причём, уязвимость выглядела довольно экзотично, но в реальности имеет прямое отношение ко многим популярным реализациям Bcrypt во множестве языков — Java, JavaScript, Python, Rust, а главное, к тому, как мы проектируем API в сфере безопасности. В этой статье я расскажу, почему это произошло, и какие практики в дизайне API помогут избежать подобных ситуаций.
Предыстория: в чём проблема с Bcrypt?
Bcrypt — это алгоритм хеширования, широко используемый для безопасного хранения паролей. Однако у него есть важное ограничение: длина входной строки (пароля или любой другой хешируемой комбинации) не должна превышать 72 байта.
Если подать на вход более длинные данные, то «лишние» символы попросту игнорируются (или, в редких случаях, библиотека может сгенерировать ошибку).
В случае с Okta этот момент привёл к неожиданной уязвимости:
- Компонент системы формировал некий «ключ» кэша, используя Bcrypt от конкатенации нескольких полей (например, userId + username + password).
- Если имя пользователя оказывалось достаточно длинным, то реальная часть пароля «сдвигалась» за предел 72-байтового лимита, и Bcrypt её не учитывал.
- Итог: злоумышленник мог подставить любой пароль, и хеш всё равно совпадал бы с сохранённым значением, ведь «лишние» байты пароля просто обрезались.
Сложно назвать это «распространённым сценарием», но практика показывает, что длинные поля (иногда это email-адреса, которые служат логином) не так уж и редки в больших компаниях. И если алгоритм не проверяет длину входных данных, последствия могут быть весьма серьёзными.
Как библиотеки в разных языках справляются с этим?
Давайте посмотрим, что делают популярные реализации Bcrypt. Для удобства я условно собрал результаты в мини-обзор:
🛡 Go
- Использует пакет golang.org/x/crypto/bcrypt.
- При попытке хешировать строку длиннее 72 байтов сразу выбрасывает ошибку ErrPasswordTooLong.
- Таким образом, «невозможно» (по умолчанию) случайно обрезать «лишние» байты: библиотека жёстко пресекает этот сценарий.
⚙️ Java (Spring Security)
- Если взять BCrypt из Spring Security, никакой ошибки при длинном пароле не возникает, и «лишние» символы фактически игнорируются.
- Другими словами, если мы хешируем строку 100 байтов, то реально в вычислении участвуют лишь первые 72.
⚙️ Java (bcrypt от Patrick Favre)
- В отличие от Spring Security, библиотека Patrick Favre [GitHub: at.favre.lib.crypto.bcrypt] жёстко ограничивает ввод и выбрасывает IllegalArgumentException, если размер входных данных превышает 72 байта.
- Это значит, что разработчик не сможет «случайно» прохешировать слишком длинную строку.
🌐 JavaScript (bcryptjs)
- При большом размере строки библиотека не выдаст ошибку, а молча обрежет данные.
- То есть для строки 74 байта будут учтены только первые 72, а оставшиеся проигнорированы.
🐍 Python (bcrypt)
- Аналогично многим: если строка слишком длинная, библиотека не жалуется, а выполняет обрезку.
- Результат — тот же потенциальный «сюрприз» для пользователя.
🦀 Rust (rust-bcrypt)
- По умолчанию происходит обрезка данных до 72 байтов.
- Хотя в исходном коде есть комментарии, объясняющие логику, чёткой ошибки о «превышении лимита» нет.
Таким образом, всё сводится к тому, что часть библиотек (Go, Favre) выбирает стратегию «жёсткого отказа», а остальные (Spring Security, bcryptjs, pyca/bcrypt, rust-bcrypt) — «тихого обрезания». В результате, если разработчики не осведомлены об этом нюансе или не читают документацию, они могут случайно внедрить подобную уязвимость в систему.
Ключевые уроки для дизайна API
На мой взгляд, ситуация с Okta — лишь иллюстрация к нескольким более общим принципам, которые стоит держать в голове при проектировании API (особенно связанного с безопасностью):
🔒 Не допускайте «неправильного» использования
Если мы знаем, что входные данные по контракту должны быть максимум 72 байта, то лучше сразу выбросить исключение/ошибку, чем молча обрезать. Такой жёсткий подход экономит уйму нервов и повышает безопасность.
✨ Будьте предсказуемы
API, которое делает неочевидные вещи («отрезаю лишние байты, но никому не скажу!»), ставит разработчика в тупик. Чем проще и прозрачнее поведение библиотеки, тем меньше вероятность «случайных» багов.
❤️ Убирайте эго и помогайте пользователям
Да, разработчик должен читать документацию. Но в реальности люди используют десятки инструментов и фреймворков в своих проектах. Чем дружелюбнее и очевиднее наше API, тем меньше риск критических ошибок.
«Вы слишком длинный пароль подали? Ошибка! Урежьте сами или используйте специальный флаг, если хотите намеренно обрезать пароль» — это куда лучше, чем «молча взял первые 72 байта и ладно…».
🧑💻 Пересматривайте дизайн со временем
Многие «молча обрезающие» реализации — это следствие наследия старых версий OpenBSD Bcrypt 90-х годов. Мир меняется, и если мы видим, что подобные нюансы становятся причиной уязвимостей, то, возможно, пора расширить функциональность, добавить жёсткие проверки или хотя бы документировать поведение жирным шрифтом.
Почему Okta не единственная?
Самое интересное, что обсуждение проблемы «Bcrypt отрезает лишнее» велось уже давно. На GitHub есть открытые задачи в репозиториях популярных реализаций:
🔗 Spring Security Issue
🔗 bcrypt.js Issue
🔗 pyca/bcrypt Issue
🔗 rust-bcrypt Issue
Но, как видим, «тихое обрезание» по-прежнему остаётся механизмом по умолчанию во многих библиотеках. Почему? Часто причина кроется в обратной совместимости: когда миллионы проектов уже используют конкретную реализацию, разработчики библиотек боятся ломать существующий функционал. Или же считают, что ответственность за длину пароля лежит на самом прикладном коде.
Личное мнение: в чём заключается «правильный» путь?
Я убеждён, что в контексте безопасности надо идти по пути Go и Java-библиотеки Patrick Favre, то есть принудительно отказывать при превышении лимита. Конечно, можно добавить флаг типа allow_truncation = true, но по умолчанию (без явного включения) должна быть жёсткая проверка. Так мы сделаем мир немного безопаснее. И когда речь идёт о разработке библиотек, то чем очевиднее поведение, тем меньше «скрытых ловушек».
Итоги и ссылки
Инцидент Okta — это не просто ошибка в одной компании, а показатель системной проблемы в экосистеме Bcrypt и нашем подходе к дизайну API. Но хорошая новость в том, что мы можем сделать выводы и улучшить собственные проекты, добавляя строгие проверки и более понятную обратную связь в коде.
Надеюсь, этот обзор и размышления были вам полезны! Если хотите узнать больше деталей об инциденте и примерах кода, рекомендую заглянуть в статью, на которой основан этот материал:
Берегите безопасность ваших пользователей и пусть все ваши API будут предсказуемыми и прозрачными!