<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
БЕЗПЕКА · MIDDLE

Як захистити вхід від перебору паролів і налаштувати rate limiting у Laravel?

Перебір закривають обмеженням частоти: у Laravel це `RateLimiter::for()` з ключем, що поєднує email і IP, плюс `throttle` middleware, який віддає 429 з `Retry-After`. Ліміт тримається в спільному кеші (Redis), а `$request->ip()` має бути справжньою адресою клієнта, тобто `TrustProxies` налаштований. Від credential stuffing ліміти допомагають мало, тому додають перевірку злитих паролів, 2FA і моніторинг, а замість постійного блокування акаунта використовують тимчасові затримки.

На форму входу йде 300 запитів на секунду з різними паролями. Що зробите в коді і що на інфраструктурі?
Ми блокуємо акаунт після пʼяти невдалих спроб. Чим це погано?
За яким ключем обмежувати спроби входу: за IP, за email чи за обома?
Після переїзду за балансувальник усі користувачі почали отримувати 429. Що сталося?
brute force rate limiting RateLimiter throttle 429 credential stuffing

Перебір паролів буває трьох видів, і від кожного захищає свій механізм. Класичний brute force бʼє в один акаунт тисячами паролів. Password spraying іде навпаки: один-два популярні паролі на тисячі акаунтів. Credential stuffing бере пари логін-пароль, злиті з інших сайтів, і пробує кожну по разу з різних IP. Розмова на співбесіді добре починається з цього розрізнення, бо одразу видно, чому одного throttle:5,1 мало. В OWASP Top 10:2025 це частина A07 Authentication Failures.

Основний інструмент у Laravel це іменовані ліміти. RateLimiter::for('login', ...) у AppServiceProvider повертає один або кілька Limit, кожен зі своїм ключем, а throttle:login на маршруті рахує спроби і при перевищенні віддає 429 з Retry-After. Ключ вирішує все: email|ip зупиняє перебір пароля одного акаунта і не блокує справжнього власника з іншої адреси, окремий ліміт за IP зупиняє spraying з однієї машини. Лічильники лежать у кеші, тому на кількох серверах кеш має бути спільним; file на кожному сервері множить ліміт на кількість серверів. Сесія для цього не підходить зовсім: бот не відправляє cookie і щоразу починає з нуля.

Ліміти за IP мають сенс, лише коли IP справжня. За балансувальником без налаштованого TrustProxies $request->ip() повертає адресу балансувальника, і весь сайт ділить один ліміт. Протилежна помилка, trustProxies(at: '*') на сервері, доступному напряму, дає клієнту змогу самому написати будь-яку адресу в X-Forwarded-For. Тому довіряють лише адресам свого балансувальника. Ліміти потрібні не лише на вході: скидання пароля, реєстрація, перевірка одноразового коду (шестизначний код без обмеження спроб перебирається за мільйон запитів) і будь-які ендпоїнти, які відправляють SMS чи листи за гроші.

Від credential stuffing ліміти допомагають мало: на кожен акаунт іде одна спроба з нової IP, і пароль часто правильний. Тут працюють Password::uncompromised(), що не дає встановити пароль із бази витоків, двофакторна автентифікація, моніторинг частки невдалих входів по всьому сайту і CAPTCHA, яку вмикають під час атаки. Окремо про блокування: «заблокувати акаунт після пʼяти невдалих спроб» звучить надійно, але дає зловмиснику змогу заблокувати будь-кого, знаючи лише email. Тимчасові ліміти із зростаючою затримкою і лист власнику про підозрілі спроби дають той самий захист без цього побічного ефекту.

// AppServiceProvider::boot()
RateLimiter::for('login', function (Request $request): array {
    $email = Str::lower((string) $request->input('email'));

    return [
        // Перебір пароля одного акаунта: 5 спроб на хвилину з однієї IP.
        Limit::perMinute(5)->by($email.'|'.$request->ip()),
        // Перебір багатьох акаунтів з однієї IP.
        Limit::perMinute(30)->by($request->ip()),
    ];
});

// Одноразовий код: мало спроб на сам код, окремо від ліміту за IP.
RateLimiter::for('otp', fn (Request $request) => Limit::perMinutes(15, 5)->by('otp|'.$request->user()->id));

// routes/web.php: 429 з Retry-After при перевищенні.
Route::post('/login', [LoginController::class, 'store'])->middleware('throttle:login');
Route::post('/two-factor', [TwoFactorController::class, 'store'])->middleware(['auth', 'throttle:otp']);

// bootstrap/app.php: справжня IP клієнта за балансувальником.
// Лише адреси балансувальника, не '*', якщо застосунок доступний напряму.
$middleware->trustProxies(at: ['10.0.0.0/8']);

// config/cache.php: лічильники в спільному Redis, а не у file на кожному сервері.
// 'limiter' => 'redis',
Що ключ ліміту визначає, від чого він захищає: email плюс IP зупиняє перебір пароля одного акаунта, IP окремо зупиняє перебір багатьох акаунтів з однієї адреси.
Що лічильник живе в кеші, і для кількох серверів кеш має бути спільним, інакше ліміт множиться на кількість серверів.
Що за проксі чи балансувальником без `TrustProxies` усі запити мають одну IP, а з довірою до будь-якого `X-Forwarded-For` зловмисник сам обирає собі IP.
Що постійне блокування акаунта після N спроб дає зловмиснику спосіб заблокувати будь-кого, тому краще тимчасові ліміти і повідомлення власнику.
Що credential stuffing (злиті пари логін-пароль з інших сайтів) іде з тисяч IP по одній спробі на акаунт і лімітами не зупиняється.
Рахувати спроби в сесії: бот просто не відправляє cookie, і кожен запит отримує нову сесію з нульовим лічильником.
Обмежувати лише за IP: ботнет з тисячею адрес отримує тисячу лімітів, а користувачі за спільним NAT офісу чи мобільного оператора блокують один одного.
Тримати ліміти в кеші `file` чи `array` на кількох серверах: кожен рахує окремо, а `array` взагалі живе один запит.
Вмикати `trustProxies(at: '*')` на сервері, доступному напряму з інтернету: заголовок `X-Forwarded-For` від клієнта стає «справжньою» IP, і ліміт за IP обходиться підстановкою заголовка.
Обмежити лише форму входу і забути скидання пароля, підтвердження одноразового коду і реєстрацію: шестизначний SMS-код без ліміту спроб перебирається за мільйон запитів.
ПОРАДА

Почніть із питання «що саме ми рахуємо і за яким ключем», а не з назви middleware. Сильна відповідь показує два ліміти на вході (на пару email+IP і на IP окремо), згадує `TrustProxies` і чесно каже, що від credential stuffing рятують не ліміти, а 2FA і перевірка злитих паролів.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Кожен ключ захищає від свого сценарію, тому на вході ставлять обидва ліміти. Бот без cookie щоразу отримує нову сесію з нульовим лічильником. Ботнет має тисячі IP, а користувачі за одним NAT ділять одну. Постійне блокування дозволяє будь-кому заблокувати чужий акаунт, знаючи лише email.