Правильное хранение паролей
Учимся хранить пароли так, чтобы даже утечка всей базы не отдала их злоумышленнику — на этом держится доверие к любому сервису.
Хеш пароля — результат одностороннего преобразования, по которому нельзя восстановить исходный пароль, но можно проверить, что введённая строка ему соответствует.
Базы данных утекают регулярно: ошибка в коде, SQL-инъекция, украденный бэкап, доступ инсайдера. Вопрос не «утечёт ли когда-нибудь таблица пользователей», а «что злоумышленник из неё достанет». Если пароли лежат в открытом виде, утечка мгновенно становится катастрофой не только для вашего сервиса, но и для всех мест, где люди использовали тот же пароль. Поэтому правильное хранение паролей — это про ограничение ущерба: база утекла, а пароли остались бесполезным набором хешей. Несанкционированный доступ к чужой базе — это статья 272 УК РФ; мы разбираем защиту, а не способ добыть чужие данные.
Зачем это знать защитнику
Категория Identification and Authentication Failures прочно держится в OWASP Top 10, и слабое хранение паролей — её классическая часть. Разработчик, который понимает, чем хеш отличается от шифрования и почему MD5 непригоден, не повторит ошибку, которую делали сотни компаний до утечек. Специалист Blue Team по этим же признакам оценивает зрелость чужого кода: увидел MD5(password) в ревью — нашёл критическую проблему.
Чего делать нельзя
Plaintext. Хранить пароль как есть — худший вариант: любой, кто получил доступ к базе или логам, сразу получает все пароли. Сюда же относится «обратимое шифрование»: если приложение умеет показать пароль обратно, значит, ключ лежит рядом и утечёт вместе с базой.
Быстрые хеши (MD5, SHA-1, SHA-256) без защиты. Это криптографические хеши, но они спроектированы быть быстрыми — а для паролей это недостаток. Современная видеокарта перебирает миллиарды кандидатов SHA-256 в секунду, поэтому короткие и популярные пароли подбираются почти мгновенно. Уязвимый код выглядит обманчиво «солидно»:
# УЯЗВИМО: быстрый хеш без соли, мгновенно ломается перебором
import hashlib
password = "hunter2"
stored = hashlib.sha256(password.encode()).hexdigest()
print(stored)
Вывод:
f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
Один и тот же пароль всегда даёт один и тот же хеш — отсюда и проблема. Беда не только в скорости. Без соли одинаковые пароли дают одинаковый хеш, поэтому из утечки сразу видно, у кого пароли совпадают, и работают заранее посчитанные таблицы (rainbow tables). Эти таблицы — словарь «популярный пароль → его хеш», и без соли они применимы к вашей базе как есть.
Как делать правильно: соль, медленный хеш, перец
Соль (salt)
Соль — случайная строка, своя для каждого пользователя, которую подмешивают к паролю перед хешированием и хранят рядом с хешем (она не секрет). Соль делает каждый хеш уникальным: даже два одинаковых пароля дают разные результаты, rainbow-таблицы становятся бесполезны (их пришлось бы пересчитывать под каждую соль отдельно), и атаковать приходится каждую запись по отдельности.
Медленный (адаптивный) хеш
Для паролей нужны специальные функции, которые намеренно медленные и умеют становиться ещё медленнее со временем: bcrypt, scrypt, Argon2. У них есть фактор стоимости (cost / work factor): чем он выше, тем больше вычислений на одну проверку. Для легального входа лишние ~200 мс незаметны, а для перебора миллиардов кандидатов это превращает задачу из «минуты» в «столетия». Argon2 и scrypt дополнительно требуют много памяти, что обесценивает дешёвый перебор на видеокартах.
# БЕЗОПАСНО (псевдокод): соль внутри, стоимость настраивается
# bcrypt сам генерирует случайную соль и кладёт её в строку хеша:
hash = bcrypt(password, cost=12)
# при логине:
ok = bcrypt_verify(input_password, stored_hash) # сравнение в постоянном времени
Важная деталь: bcrypt/Argon2 встраивают соль и параметры прямо в строку хеша. Поэтому отдельную колонку для соли заводить не нужно — алгоритм при проверке достанет её из сохранённого значения. Никогда не пишите хеш-функцию для паролей сами: используйте проверенную библиотеку (например, bcrypt, argon2-cffi, или passlib в Python).
Перец (pepper)
Перец — общий секрет, который подмешивают ко всем паролям, но, в отличие от соли, не хранят в базе: его держат отдельно (в конфиге приложения, переменной окружения, секрет-менеджере или HSM). Идея в том, что при утечке только базы у атакующего нет перца, и перебор невозможен без него. Перец — это дополнительный слой поверх соли и медленного хеша, а не замена им.
Как это работает под капотом
При регистрации приложение берёт пароль, генерирует случайную соль, прогоняет «пароль + соль» через адаптивный хеш с заданной стоимостью и сохраняет получившуюся строку (в ней уже зашиты алгоритм, стоимость и соль). При входе оно достаёт эту строку, извлекает из неё параметры и соль, хеширует введённый пароль теми же параметрами и сравнивает результаты. Само сравнение делают в постоянном по времени режиме (constant-time), чтобы по длительности ответа нельзя было угадывать хеш по символам. Восстановить исходный пароль из хеша нельзя в принципе — функция односторонняя; можно лишь проверить кандидата, а адаптивная стоимость делает массовую проверку кандидатов невыгодной.
Как защититься
- Только адаптивный хеш с солью. Argon2id (предпочтительно), scrypt или bcrypt с разумным cost-фактором. Никогда не plaintext, не обратимое шифрование и не «голый» MD5/SHA.
- Доверьте детали библиотеке. Генерацию соли, формат хранения и constant-time сравнение делает проверенная реализация. Свой велосипед почти гарантированно содержит ошибку.
- Калибруйте стоимость под своё железо и повышайте её со временем: вход должен занимать заметную, но приемлемую долю секунды. Предусмотрите бесшовное обновление: при успешном входе можно перехешировать пароль с новыми параметрами.
- Защита от перебора на входе. Хеширование защищает утёкшую базу; живую форму логина защищают отдельно: ограничение числа попыток (rate limiting) и временная блокировка по аккаунту/IP, экспоненциальные задержки, CAPTCHA при подозрительной активности. Это отбивает онлайн-подбор и credential stuffing.
- Поддержка длинных парольных фраз и проверка по утёкшим спискам. Разрешайте длинные пароли и не режьте спецсимволы; отклоняйте пароли из публичных утечек. Многофакторная аутентификация (MFA) резко снижает ценность украденного пароля.
- Обнаружение. Всплеск неудачных входов, попытки по многим аккаунтам с одного источника, аномальная география — сигналы атаки на пароли. Логируйте и алертите.
Юридическое напоминание: проверять стойкость хранения паролей можно только в своей системе или в рамках легального пентеста с письменным разрешением. Брутфорс чужих учёток наказуем (ст. 272 УК РФ).
Итоги
- Цель правильного хранения — чтобы утечка базы не выдала пароли; хеш необратим, в отличие от шифрования.
- Plaintext, обратимое шифрование и быстрые хеши (MD5/SHA) без защиты — недопустимы: они ломаются перебором.
- Соль делает каждый хеш уникальным и убивает rainbow-таблицы; адаптивный хеш (Argon2/scrypt/bcrypt) делает перебор невыгодным; перец добавляет секрет вне базы.
- Не пишите хеширование сами — берите проверенную библиотеку; калибруйте и повышайте стоимость со временем.
- Форму логина защищают отдельно: rate limiting, блокировки, MFA и мониторинг неудачных попыток.