DEV Community

Denis Augusto
Denis Augusto

Posted on

Deixei um bot derrubar minha API antes de descobrir o throttle

Segunda de manhã, servidor no chão, e eu sem entender nada

A cena: segunda-feira, 8h. Abro o monitoramento e o servidor está gritando. CPU no talo, banco engasgado, aplicação lenta pra todo mundo.

Meu primeiro palpite? "Bombou o projeto, viralizou!" 🎉

Aí olhei o log. Não era gente. Era um único IP batendo no meu endpoint de login. Milhares de requisições por minuto, tentando senha atrás de senha. Um bot fazendo força bruta enquanto eu dormia.

A pior parte: eu não tinha nada pra segurar isso. Meu endpoint aceitava requisição de quem quisesse, quantas vezes quisesse. A porta estava escancarada.

Já parou pra pensar quantas requisições um usuário consegue disparar na sua API agora, sem nenhum limite? Pois é.

O problema: sua API confia em todo mundo

Por padrão, uma rota Laravel não tem limite nenhum. Ela responde a qualquer um, o quanto vier:

Route::post('/login', [AuthController::class, 'login']);
// Pode receber 5 requisições por dia ou 50 mil por minuto.
// Pra ela tanto faz. 🤷
Enter fullscreen mode Exit fullscreen mode

Isso é convite pra três dores de cabeça:

  • Força bruta no login, tentando senha até acertar.
  • Scraping dos seus dados, alguém raspando tudo de uma vez.
  • Custo: cada requisição gasta CPU, banco e, se você estiver na nuvem, dinheiro.

A solução tem nome e é embutida no Laravel: rate limiting. Ou seja, "você só pode fazer X requisições por minuto. Passou disso, toma um 429 e espera".

A versão de 3 linhas: middleware throttle

O jeito mais rápido de proteger uma rota é o middleware throttle. Você diz quantas requisições por quantos minutos:

Route::middleware('throttle:60,1')->group(function () {
    Route::apiResource('posts', PostController::class);
});
// 60 requisições por 1 minuto, por usuário/IP. Passou disso, 429.
Enter fullscreen mode Exit fullscreen mode

Pronto. Se um IP passar de 60 requisições no minuto, o Laravel responde com 429 Too Many Requests sozinho, sem você escrever uma linha de lógica. O bot que estava me atormentando teria batido na parede.

E se você tem uma API, provavelmente já tem um limiter chamado api funcionando sem você saber, aplicado no grupo de rotas de routes/api.php. Vale conferir.

A versão profissional: named limiters

O throttle:60,1 resolve o básico. Mas e quando você quer regras diferentes por tipo de rota, ou por tipo de usuário? Aí entram os named limiters.

Você define a regra num lugar central (no boot() do seu AppServiceProvider) e dá um nome pra ela:

use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('api', function (Request $request) {
        // 60 por minuto, contados por usuário logado — ou por IP se for visitante
        return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
    });
}
Enter fullscreen mode Exit fullscreen mode

Repara no ->by(): é ele que decide quem conta pra cota. Usando o ID do usuário, cada pessoa tem sua própria cota; caindo pro IP quando é visitante. Sem isso, todo mundo dividiria o mesmo balde — um abusador estouraria o limite de todos os outros.

Depois é só apontar a rota pro limiter pelo nome:

Route::middleware('throttle:api')->group(function () {
    // suas rotas protegidas
});
Enter fullscreen mode Exit fullscreen mode

Como usar na prática

Protegendo o login (o meu caso). Aqui o pulo do gato é contar por e-mail, não só por IP, pra travar força bruta mesmo que o bot troque de IP:

RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(500),                          // teto geral da rota
        Limit::perMinute(5)->by($request->input('email')), // 5 tentativas por e-mail
    ];
});
Enter fullscreen mode Exit fullscreen mode

Cinco tentativas erradas naquele e-mail e a conta fica trancada por um minuto. Bot de força bruta chora.

Limite diferente por plano. Cliente VIP não deveria ter a mesma cota do plano grátis. Dá pra decidir na hora:

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->vip
        ? Limit::none()          // VIP sem limite
        : Limit::perHour(10);    // grátis, 10 por hora
});
Enter fullscreen mode Exit fullscreen mode

Mensagem de erro amigável. Em vez do 429 seco, você pode devolver uma resposta customizada:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->response(function (Request $request, array $headers) {
        return response()->json(['message' => 'Calma lá! Tenta de novo em instantes.'], 429, $headers);
    });
});
Enter fullscreen mode Exit fullscreen mode

Pegadinha: os headers que ninguém lê (mas deveria)

Toda resposta com rate limit vem com headers que contam a história: X-RateLimit-Limit (seu teto) e X-RateLimit-Remaining (quanto ainda sobra). Quando estoura, vem também o Retry-After, dizendo em quantos segundos pode tentar de novo.

Se você consome uma API de terceiros e vive tomando 429, é aí que você olha. E se você expõe uma API, esses headers são o que permite o cliente se comportar bem sozinho, sem ficar batendo na parede.

Outra pegadinha: rate limiting usa o cache pra contar. Em produção, garante que seu cache está num driver de verdade (Redis, de preferência), não no array, senão a contagem se perde a cada requisição.

Bônus: e agora?

Rate limiting é a porta de entrada de uma API saudável, mas não para por aí:

  • Combine com throttle no login e você já matou 90% dos ataques de força bruta.
  • Use limites diferentes pra rotas públicas (mais apertado) e autenticadas (mais folgado).
  • Se sua fila (queue) chama APIs externas, aplique rate limiting nos jobs também — o Laravel tem Redis::throttle() pra isso.

Não precisa fazer tudo de uma vez. Começa protegendo o login e as rotas mais sensíveis. O resto vem depois.

Antes de você fechar a aba

Eu aprendi na marra: API sem rate limiting é porta destrancada. Funciona lindamente até o dia que um bot resolve entrar.

O melhor de tudo? A proteção básica é literalmente uma linha de middleware. Não tem desculpa pra deixar a porta aberta.

E aí, suas rotas de login e API já têm rate limiting? Ou você também só vai descobrir isso quando o servidor cair numa segunda de manhã? Conta nos comentários. 🔒


Curtiu? Tem mais conteúdo sobre Laravel limpo e seguro por aqui. Me segue pra não perder os próximos.

Top comments (0)