Как хранить ответы на security questions, чтобы они не стали уязвимостью

Ответы на security questions нужно хранить так же осторожно, как пароли: не в открытом виде, не в логах, не в админке и не в письмах. Правильная схема — превращать ответ в криптографический хеш с помощью медленной функции хеширования, добавлять уникальный salt и защищать процесс отдельным секретом приложения, если это возможно.

Если ответ на контрольный вопрос можно прочитать из базы, показать сотруднику поддержки или восстановить по запросу — это уже не секрет, а будущая утечка.

Почему обычные ответы на вопросы так часто ломаются

Контрольные вопросы придумывали как простой способ восстановить доступ: “кличка собаки”, “город рождения”, “девичья фамилия матери”. Проблема в том, что эти ответы часто не секретные. Их можно угадать, найти в соцсетях, узнать из слитых баз или подобрать по словарю.

Даже если сама база не утекла, слабые ответы ломаются через форму восстановления. Пользователь ввёл “Барсик” — злоумышленник тоже попробует “Барсик”. Если система не ограничивает попытки, не замедляет проверку и не требует других факторов, такой ответ превращается в слабый пароль из одного слова.

Поэтому хранение ответов — это только часть задачи. Надёжная схема состоит из трёх вещей:

  • пользователь вводит ответ с достаточной случайностью, а не очевидный факт из жизни;
  • сервер хранит только хеш ответа, а не сам ответ;
  • проверка ответа ограничена по скорости, количеству попыток и защищена от утечек через логи, админку и поддержку.

Что именно нужно хранить в базе

В базе не должно быть поля вроде answer со значением “Москва”, “Fluffy” или “Ivanova”. Вместо этого храните параметры, которые позволяют проверить ответ, но не восстановить его.

Поле Что хранить Зачем это нужно
id Уникальный идентификатор записи Чтобы безопасно связывать ответ с пользователем и вопросом
user_id ID пользователя Чтобы найти ответ только для нужного аккаунта
question_id ID вопроса или тип пользовательского вопроса Чтобы отличать несколько security questions у одного пользователя
salt Случайная строка 16–32 байта для каждого ответа Чтобы одинаковые ответы у разных пользователей давали разные хеши
kdf Название функции: например Argon2id, bcrypt, scrypt или PBKDF2 Чтобы при проверке использовать тот же алгоритм
kdf_params Память, количество итераций, параллелизм и другие параметры Чтобы можно было проверить ответ и позже усилить защиту
answer_hash Результат хеширования ответа Это единственная “версия” ответа, которая должна храниться
pepper_version Версия дополнительного секретного ключа приложения Чтобы безопасно менять pepper без потери старых ответов

Сам ответ после обработки должен исчезать из памяти приложения. Не сохраняйте его в логах, ошибках валидации, тикетах поддержки, аналитике событий или дампах отладки.

Какой способ хранения выбрать

Если коротко: для нового проекта берите salted slow KDF, желательно Argon2id, и добавляйте pepper. Остальные варианты либо опасны, либо подходят только как временный костыль.

Подход Что произойдёт при утечке базы Когда использовать
Хранить ответ открытым текстом Ответы сразу становятся доступны тому, кто получил базу Не использовать никогда
Шифровать ответы и иметь возможность расшифровать При компрометации ключа или приложения ответы можно прочитать Только в legacy-системах, пока идёт миграция
MD5, SHA-1 или обычный SHA-256 без KDF Слабые ответы подбираются очень быстро через офлайн-атаку Не использовать для секретов
Salt + bcrypt, scrypt, Argon2id или PBKDF2 Утечка базы не даёт готовые ответы, подбор становится дорогим Базовый рабочий вариант для production
Salt + slow KDF + pepper Даже при утечке БД злоумышленнику нужен ещё и секрет приложения Лучший практический вариант для новых систем
Fuzzy matching: “Москва”, “москва”, “Moscow” считаются одним ответом Система сама расширяет число допустимых вариантов для атаки Лучше избегать; максимум — заранее выбранная строгая нормализация

Как должна выглядеть рабочая схема

  1. Пользователь задаёт ответ. Лучше не просить его вспоминать факт из биографии. Пусть ответ будет случайной фразой или строкой, которую удобно хранить в менеджере паролей.
  2. Сервер применяет одинаковую нормализацию. Например, если вы решили обрезать пробелы по краям — делайте это и при создании, и при проверке. Не надо автоматически менять регистр, убирать знаки препинания, транслитерировать или искать “похожие” ответы.
  3. Генерируется уникальный salt. Salt не обязан быть секретным, но должен быть случайным и отдельным для каждого ответа.
  4. Ответ хешируется медленной функцией. Для новых систем хороший выбор — Argon2id. Если используете bcrypt, scrypt или PBKDF2, подбирайте параметры так, чтобы проверка занимала ощутимые миллисекунды, а не проходила мгновенно.
  5. Добавляется pepper. Pepper — это отдельный секрет приложения, который хранится не в базе, а в KMS, secrets manager или защищённом хранилище. Его не должно быть в репозитории и рядом с дампом базы.
  6. В базу пишется только хеш и параметры. Сам ответ не сохраняется.
  7. При проверке используется constant-time сравнение. Это снижает риск утечки через время ответа и защищает от грубых ошибок реализации.
  8. Попытки ограничиваются. Делайте rate limit, задержки или блокировки по аккаунту, IP, устройству и вопросу. Но не превращайте защиту в удобный инструмент для DoS-атаки.

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

Что делать с самими ответами пользователя

Самая частая ошибка — думать, что если хеш хороший, то можно спокойно просить пользователя ввести “имя первого учителя”. Нет. Хранение защищает от утечки базы, но не спасает от угадывания через форму восстановления.

Лучше менять саму логику security questions:

  • разрешайте пользователю задавать свой вопрос;
  • не привязывайте ответ к публичным фактам;
  • подсказывайте, что ответ можно сделать случайным;
  • не ограничивайте ответ слишком короткой длиной;
  • не показывайте ответ после ввода;
  • не отправляйте ответ на почту и в SMS;
  • не давайте сотрудникам поддержки доступ к ответам.

Практичный пример: вместо вопроса “кличка питомца” пользователь может выбрать вопрос “какую случайную фразу я записал для восстановления аккаунта?” и ответить чем-то вроде river-lamp-27-blue. Такой ответ не нужно помнить наизусть — его можно хранить в менеджере паролей вместе с логином и основным паролем.

Как проверять ответ без новых дыр

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

Не стоит показывать слишком много деталей. Фразы вроде “неверная кличка собаки” или “ответ не совпадает с городом рождения” помогают атакующему понять, какой вопрос пробовать дальше. Лучше давать нейтральный ответ: “Не удалось подтвердить данные для восстановления”.

Также не храните подсказки к ответам в открытом виде. Подсказка “первый питомец, коричневый, звали на букву Б” резко сужает пространство для подбора. Если без подсказок совсем нельзя, делайте их максимально общими и не храните рядом с хешем как отдельный бонус для атакующего.

Если ответы уже лежат в базе открытым текстом

Не делайте вид, что простое хеширование решит проблему. Если пользователи раньше вводили “Moscow1987”, “Barsik” или фамилию родственника, эти ответы уже могли попасть в чужие руки: через логи, бэкапы, админку, экспорт в Excel или обращения в поддержку.

План миграции такой:

  1. Найдите все места, где ответы реально хранятся: основная база, реплики, бэкапы, логи, тикеты, BI-выгрузки, старые CSV-файлы.
  2. Создайте новую схему хранения с salt, KDF, параметрами и pepper.
  3. Не переносите старые ответы в новую систему как “безопасные”. Если есть plaintext — пересчитайте хеши, но считайте старые ответы потенциально скомпрометированными.
  4. Попросите пользователей задать новые случайные ответы в уже авторизованной сессии.
  5. Удалите старые plaintext-поля и убедитесь, что приложение больше не пишет ответы в логи.
  6. Проверьте бэкапы. Если старые ответы попали в бэкапы, планируйте их ротацию или дождитесь истечения срока хранения, но учитывайте риск.
  7. Добавьте rate limit и мониторинг подозрительных попыток восстановления.

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

Что выбрать в зависимости от ситуации

  • Если вы запускаете новый продукт: лучше вообще не использовать security questions как основной способ восстановления. Если бизнес требует — храните ответы через Argon2id или другой slow KDF, добавляйте salt и pepper, а пользователю предлагайте случайный ответ.
  • Если у вас уже есть plaintext: срочно убирайте открытое хранение, переносите ответы в новую схему и просите пользователей задать новые значения. Не отправляйте старые ответы пользователям и не показывайте их в админке.
  • Если аккаунты высокоценные: не полагайтесь на контрольные вопросы. Используйте recovery codes, аппаратные ключи, подтверждение через уже доверенное устройство или ручную верификацию с несколькими независимыми факторами.
  • Если есть отдел поддержки: сотрудники не должны видеть ответы. Поддержка может запускать другой процесс восстановления, но не должна получать доступ к секрету пользователя.
  • Если продукт внутренний и риски ниже: всё равно не храните ответы открытым текстом. Минимум — bcrypt или PBKDF2 с нормальными параметрами, уникальный salt и rate limit.
  • Если пользователи часто забывают ответы: не превращайте это в повод хранить ответы в читаемом виде. Лучше добавьте recovery codes, резервные методы входа или понятный процесс повторной настройки после подтверждения личности.

Частые ошибки при хранении security question answers

  • Хранить ответы как обычный текст. Это самый прямой путь к компрометации при утечке базы.
  • Использовать SHA-256 без KDF. Для паролей и ответов на вопросы обычный быстрый хеш не подходит.
  • Использовать один salt на всех. Тогда одинаковые ответы дают одинаковые хеши, а атакующему проще готовить словари.
  • Нормализовать ответ слишком агрессивно. Приведение к нижнему регистру, удаление пробелов и “поиск похожего ответа” уменьшают стойкость.
  • Показывать ответы в админке. Даже если интерфейс внутренний, доступ к нему часто шире, чем кажется.
  • Писать ответы в логи. Ошибка валидации, debug-дамп или exception payload могут незаметно сохранить секрет.
  • Отправлять ответы на почту. Почтовый ящик часто менее защищён, чем аккаунт, который вы пытаетесь восстановить.
  • Разрешать бесконечные попытки. Для угадываемого ответа это почти подарок атакующему.
  • Считать несколько слабых вопросов сильной защитой. Если каждый ответ можно найти в соцсетях, три таких ответа не становятся надёжным секретом.

Практические рекомендации для внедрения

  • Используйте готовые библиотеки для Argon2id, bcrypt, scrypt или PBKDF2. Не пишите собственную криптографию.
  • Храните salt отдельно для каждого ответа, а не один на пользователя или на всю таблицу.
  • Добавляйте pepper через KMS или secrets manager, а не через переменную в репозитории.
  • Версионируйте параметры хеширования, чтобы позже можно было перейти на более стойкие настройки.
  • Проверяйте, что ответы не попадают в логи, мониторинг, трейсы, аналитику и сообщения об ошибках.
  • Ограничивайте попытки восстановления и отслеживайте аномалии: много запросов с одного IP, перебор вопросов, частые ошибки по одному аккаунту.
  • Не используйте security questions как единственный способ сброса пароля для важных аккаунтов.
  • Если ответ забыт, не показывайте его и не восстанавливайте — предлагайте безопасный процесс повторной настройки.

Итог

Правильное хранение ответов на security questions выглядит просто: не хранить сам ответ, использовать уникальный salt, медленную функцию хеширования, pepper и строгую проверку без лишних подсказок. Но этого недостаточно, если пользователь отвечает очевидными фактами из жизни. Лучший практический подход — превратить security question в дополнительный пароль: случайный ответ, менеджер паролей, хеш вместо plaintext и ограничение попыток восстановления.

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

Оцените статью
PEFile — Безопасность и технологии простым языком