Защита входа: перебор, лимиты и 2FA

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

Brute force — перебор паролей к одному аккаунту. Credential stuffing — массовая проверка пар «логин-пароль», утёкших с других сайтов.

Почему это важно

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

Эти две поверхности атаки полезно разделять в голове. Хеширование, соль и work factor из предыдущего урока работают только в одном сценарии — когда у злоумышленника уже есть дамп базы и он перебирает хеши офлайн. Но огромная доля реальных взломов аккаунтов идёт совсем иначе: без всякого доступа к базе атакующий просто шлёт запросы на вашу публичную форму входа. Здесь сила алгоритма хеширования значения почти не имеет — важно лишь, сколько попыток вы позволите сделать и как быстро. Поэтому защита входа и защита хранилища паролей — это две независимые задачи, и наличие одной не отменяет необходимости другой.

Особенно опасен credential stuffing, потому что он не «угадывает» пароли, а проверяет заведомо реальные пары, утёкшие с других сервисов. Люди переиспользуют пароли, и если ваш пользователь применил тот же пароль, что и на взломанном где-то сайте, атакующему достаточно одной верной попытки. Такой трафик тяжело отличить от обычного: каждая отдельная попытка выглядит как честный вход с правильным на вид паролем — просто их миллионы и по разным аккаунтам. Это объясняет, почему одной только блокировки «после пяти неудач к одному аккаунту» недостаточно: stuffing бьёт по одному разу в каждый из тысяч аккаунтов и под такой порог не подпадает.

Защита 1: ограничение скорости и блокировки

Считайте неудачные попытки и реагируйте: задержка между попытками, временная блокировка аккаунта или IP, экспоненциальный backoff. Цель — сделать перебор настолько медленным, что он теряет смысл.

// Концептуально: растущая задержка после неудач
fails = store.get(user) or 0
if (fails >= 5) {
  delay = min(2 ** (fails - 4), 60)   // 2с, 4с, 8с ... до минуты
  sleep(delay)
}
// при успехе -> сбросить счётчик; при неудаче -> увеличить

Учитывайте оба измерения: по аккаунту (против перебора одного пароля) и по IP/сети (против credential stuffing по многим аккаунтам). Блокировку аккаунта применяйте осторожно — иначе ею можно устроить отказ в обслуживании честному пользователю; часто лучше CAPTCHA и задержки.

Защита 2: нейтральные сообщения

Не подсказывайте атакующему, существует ли логин. «Неверный логин» против «неверный пароль» выдаёт, какие учётки есть (user enumeration). Отвечайте одинаково.

// Уязвимо: раскрывает существование аккаунта
if (!userExists) return "Пользователь не найден";
if (!passwordOk) return "Неверный пароль";

// Безопасно: одинаковый ответ в обоих случаях
if (!userExists || !passwordOk) return "Неверный логин или пароль";

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

Защита 3: второй фактор (2FA/MFA)

Даже верный пароль не должен быть единственным ключом. Второй фактор требует что-то ещё: одноразовый код из приложения (TOTP), аппаратный ключ (WebAuthn/FIDO2), реже — код в сообщении. Тогда украденный пароль сам по себе бесполезен.

Факторы аутентификации:
  знание   — пароль, PIN
  владение — телефон с TOTP, аппаратный ключ
  свойство — отпечаток, лицо
MFA = минимум два РАЗНЫХ фактора (пароль + TOTP), а не два пароля

Предпочтительны TOTP-приложения и аппаратные ключи; коды по SMS слабее (перехват, подмена SIM), но всё же лучше, чем ничего.

У второго фактора есть важное свойство, которое часто упускают: он должен быть устойчив к фишингу. Код TOTP, как и пароль, можно выманить — поддельный сайт попросит ввести и пароль, и шестизначный код, тут же переправит их на настоящий сервис и войдёт. Аппаратные ключи по стандарту WebAuthn/FIDO2 решают и эту проблему: ключ криптографически привязывает вход к конкретному домену, и на поддельном адресе он просто откажется работать, потому что домен не совпал. Поэтому, выстраивая 2FA, полезно различать факторы не только по «что-то ещё», но и по их стойкости к обману пользователя.

Не забудьте и про сценарии восстановления — именно через них чаще всего обходят 2FA. Если пользователь потерял телефон, ему нужен запасной путь: одноразовые коды восстановления, выданные при настройке, или резервный фактор. Но этот путь становится новой дверью: если «забыли второй фактор» сводится к отправке кода на тот же email, который защищён лишь паролем, вся ценность 2FA испаряется. Резервные коды храните на сервере так же бережно, как пароли (хешируйте их), и относитесь к процедуре восстановления как к полноценной точке входа, требующей собственной защиты.

Как работает под капотом: TOTP

TOTP вычисляет 6-значный код из общего секрета и текущего времени, нарезанного на 30-секундные окна. Сервер и приложение хранят один секрет и независимо считают один и тот же код; он меняется каждые 30 секунд, поэтому даже подсмотренный код быстро устаревает.

Раз код зависит от времени, у схемы есть две практические тонкости. Во-первых, часы на сервере и на телефоне могут немного разойтись, поэтому сервер обычно принимает не только текущее окно, но и соседнее — это «окно допуска», компромисс между удобством и строгостью. Во-вторых, поскольку код действует целых 30 секунд, его всё же можно успеть подсмотреть и переиспользовать в этом промежутке; чтобы закрыть это, сервер запоминает уже использованные коды и не принимает один и тот же код дважды. И главное: общий секрет TOTP — это, по сути, второй пароль; его хранят на сервере с тем же уровнем заботы, а пользователю показывают (в виде QR-кода) ровно один раз при настройке.

Частые ошибки

  • Безлимитные попытки входа. Открытая дверь для перебора и stuffing.
  • Разные сообщения об ошибке. Выдают существование аккаунтов.
  • Только SMS как 2FA. Уязвим к перехвату; предпочитайте TOTP/аппаратные ключи.
  • Жёсткая блокировка аккаунта. Превращается в инструмент DoS против пользователя.
  • Слабое восстановление 2FA. Если обойти второй фактор можно через email под одним лишь паролем, защита иллюзорна.
  • Приём одного TOTP-кода несколько раз. В пределах 30-секундного окна код можно переиспользовать — отмечайте использованные.

Итоги

  • Хеширование защищает утёкшую БД; форму входа защищают лимиты и задержки.
  • Нейтральные сообщения и стабильное время ответа скрывают существование аккаунтов.
  • 2FA делает украденный пароль недостаточным; предпочтительны TOTP и аппаратные ключи.
Проверьте себя
1. Чем credential stuffing отличается от обычного брутфорса?
AЭто одно и то же
BStuffing массово проверяет утёкшие с других сайтов пары логин-пароль по многим аккаунтам, а брутфорс перебирает пароли к одному
CStuffing атакует только базу данных
DБрутфорс работает лишь по SMS
2. Почему сообщения «пользователь не найден» и «неверный пароль» опасны?
AОни слишком длинные
BОни позволяют перечислить существующие аккаунты (user enumeration); ответ должен быть нейтральным и одинаковым
CОни замедляют вход
DОни нарушают локализацию
3. Что обеспечивает двухфакторная аутентификация?
AУдвоение длины пароля
BТребование второго, иного фактора (например, TOTP или аппаратного ключа), из-за чего украденный пароль сам по себе бесполезен
CШифрование базы паролей
DУскорение входа