Skip to content
This page has been auto-translated and may contain errors.View in English

Identificando clientes

Um limite de cinco requisições por minuto não significa nada até você dizer cinco por o quê.

O rate limiting só funciona se você conseguir identificar de forma consistente quem está fazendo a requisição, e o identificador escolhido decide quatro coisas ao mesmo tempo:

  • Justiça. Você está limitando a entidade certa?
  • Eficácia. Alguém consegue driblar o limite?
  • Experiência. Usuários legítimos estão sendo penalizados?
  • Segurança. O próprio identificador pode ser explorado de forma indevida?

A pergunta por trás de todo rate limit: quem ou o que você está realmente tentando limitar?

Endereço IP

O padrão, e o que você obtém sem nem pedir. O express-rate-limit usa o endereço do cliente a menos que você diga o contrário, que é exatamente o que o limitador do capítulo anterior vinha fazendo.

Bom para endpoints públicos sem login, prevenção básica de abuso e conter enchentes simples de requisições.

O atrativo é que ele sempre existe. Não precisa de autenticação, funciona para tráfego anônimo, é simples, e um usuário comum não consegue mudá-lo por capricho.

O problema é que endereços são compartilhados e endereços se movem. Centenas de funcionários atrás de uma única conexão de escritório parecem um único cliente, assim como todo mundo em uma biblioteca e um grande número de usuários móveis atrás de uma operadora.

Enquanto isso, um usuário com endereço variável ganha um orçamento novo cada vez que ele muda, e o IPv6 distribui muitos endereços dentro de uma única sub-rede.

Tornando a escolha explícita em vez de simplesmente herdá-la:

js
import { rateLimit, ipKeyGenerator } from 'express-rate-limit'

const limiter = rateLimit({
  limit: 5,
  windowMs: 60000,
  keyGenerator: (req, res) => ipKeyGenerator(req.ip),
})

keyGenerator retorna a string contra a qual uma requisição é contabilizada. Esse é o gancho que tudo o que vem a seguir utiliza.

JunoEndereço IP A armadilha é que um endereço parece uma pessoa, mas não é. Ele identifica uma conexão com a internet, e uma conexão pode carregar uma pessoa ou um prédio inteiro.

Todo problema dessa estratégia vem dessa diferença.

JunoEndereço IP Atrás de um proxy ou load balancer, req.ip é o endereço do proxy, então toda requisição parece vir de um único cliente e seu limite acaba se aplicando ao mundo inteiro de uma vez.

O Express precisa de app.set('trust proxy', ...) para ler o endereço encaminhado em vez disso. Configure com o número de proxies que você realmente opera, porque confiar cegamente no cabeçalho permite que um cliente falsifique qualquer endereço que quiser e driblar o limite por completo.

JunoEndereço IP O IPv6 torna o limite por endereço praticamente inútil se aplicado de forma ingênua, porque um único assinante costuma receber um /64, o que representa mais endereços do que toda a internet IPv4. Limitar por endereço significa limitar por um endereço que ele ainda nem usou.

A resposta é limitar por prefixo. O express-rate-limit 8 expõe ipv6Subnet para isso, com valor padrão 56, onde números menores agrupam de forma mais agressiva.

O auxiliar ipKeyGenerator existe pelo mesmo motivo: ele normaliza os endereços para que clientes IPv4 e IPv6 sejam identificados de forma consistente. Escrever req.ip diretamente na chave é o que silenciosamente para de funcionar no dia em que chega tráfego IPv6.

ID do usuário autenticado

Depois que alguém faz login, você sabe exatamente quem é essa pessoa, seja por uma sessão, uma claim de JWT ou uma consulta ao banco de dados.

Bom para endpoints autenticados, cotas por usuário e planos escalonados.

Cada usuário real tem seu próprio orçamento, então ninguém interfere no limite de ninguém. Esse identificador acompanha o usuário entre dispositivos e redes, e trocar de endereço não muda nada, porque a identidade não é a conexão.

O que ele não consegue fazer é proteger nada antes do login. Cadastro, redefinição de senha e endpoints públicos acontecem todos antes de existir um id de usuário, e são exatamente esses os endpoints que o abuso costuma encontrar.

js
const limiter = rateLimit({
  limit: 3,
  windowMs: 10000,
  keyGenerator: (req) => req.user.id,
})

app.get('/api/data', authenticate, limiter, handler)

A ordem importa. O Express executa os middlewares da esquerda para a direita, então a autenticação precisa definir req.user antes que o limitador o leia. Coloque o limitador primeiro e ele vai ler undefined para todo mundo, associando todas as requisições ao mesmo grupo.

JunoID do usuário autenticado Rode três usuários contra um limite de três a cada dez segundos e o efeito é imediato. Cada um recebe seus próprios três, e um deles esbarrar no limite não afeta os outros em nada.

Isso é justiça funcionando. Com endereços, três pessoas no mesmo escritório estariam compartilhando um único orçamento de três.

JunoID do usuário autenticado A ordem dos middlewares é o bug que vale a pena prever aqui, e ele falha silenciosamente. Um limitador que lê req.user.id antes de a autenticação rodar vai lançar um erro por causa do undefined ou vai associar todo mundo ao mesmo valor, o que resulta em um único limite compartilhado por toda a sua base de usuários.

Nenhum dos dois problemas aparece em um teste de caminho feliz, porque com um único usuário no teste não há diferença visível.

JunoID do usuário autenticado Limites baseados em usuário empurram o abuso um passo para trás: se as contas são gratuitas e o cadastro é ilimitado, o identificador fica tão barato de obter quanto um endereço. Proteger o cadastro com um limite baseado em endereço é o que fecha essa brecha, o que é um bom argumento para usar os dois em vez de escolher apenas um.

Os limites também passam a fazer parte do produto quando são por usuário. Planos gratuitos e pagos com orçamentos diferentes exigem que o limite seja uma função da requisição, não uma constante, e a consulta ao plano do usuário passa a ficar no caminho crítico de cada requisição. Faça cache disso.

Chave de API e ID de sessão

Mais dois identificadores, cada um adequado a um tipo diferente de chamador.

Chave de APIID de sessão
Bom paraIntegrações de terceiros, APIs corporativas, planos escalonadosAplicações web com sessões, carrinhos, checkout e fluxos de formulário
Pontos fortesVincula-se a uma aplicação específica, suporta níveis de plano, é revogável em caso de abuso, simples de rastrearFunciona tanto para usuários logados quanto para visitantes, mais persistente que um endereço, acompanha toda uma jornada
Pontos fracosChaves são compartilhadas e vazam, uma única chave pode atender milhares de usuários finais, e exige gerenciamento de chavesLimpar os cookies reinicia a sessão, a fixação de sessão é um risco, e ela simplesmente não existe em APIs sem estado

Uma chave de API torna o escalonamento por níveis natural, porque limit pode ser uma função da requisição:

js
import { getUserTier } from './services/billing.js'

const limiter = rateLimit({
  windowMs: 60000,
  keyGenerator: (req) => req.headers['x-api-key'],
  limit: (req) => (getUserTier(req.headers['x-api-key']) === 'pro' ? 1000 : 100),
})

Um id de sessão é lido da mesma forma, a partir de req.session.id.

JunoChave de API e ID de sessão Quatro identificadores, e nenhum deles é o "correto". Cada um identifica um tipo diferente de chamador.

Uma chave de API identifica uma aplicação. Um id de usuário identifica uma pessoa. Uma sessão identifica uma visita. Um endereço identifica uma conexão. Escolha o que combina com o que você está limitando.

JunoChave de API e ID de sessão O ponto fraco da chave de API surpreende muita gente: uma chave identifica a integração, não a pessoa que a está usando.

Um parceiro com cinquenta mil usuários finais tem uma única chave, então um único usuário problemático consome o orçamento de todos e faz a integração inteira ser limitada.

Se isso importa, a chave precisa de um orçamento por chave mais um orçamento por usuário final dentro dela, o que significa repassar um identificador de usuário final através da integração.

JunoChave de API e ID de sessão Qualquer coisa que o cliente controla pode ser trocada, e ids de sessão são os piores nesse quesito: limpar os cookies não custa nada e gera um orçamento novo, então limites baseados em sessão desestimulam acidentes, não atacantes.

Duas coisas valem a pena independentemente do que você usa como chave. Nunca coloque um segredo bruto na chave, já que chaves de limitador acabam parando em logs e dumps de armazenamento, então faça hash da chave de API antes.

E decida o que acontece quando o identificador está ausente, porque um keyGenerator que retorna undefined agrupa todas essas requisições em um único grupo. Isso tanto pode ser um catch-all útil quanto um limite compartilhado por acidente, e a decisão deveria ser sua.

Combinando os identificadores

Sistemas reais raramente escolhem apenas um. O arranjo mais comum é uma cadeia com alternativas de reserva, do mais específico para o menos específico:

  1. ID do usuário autenticado, quando alguém está logado.
  2. Chave de API, para integrações.
  3. ID de sessão, para visitantes com uma sessão.
  4. Endereço IP, como último recurso.

Cada passo abaixo é menos preciso e mais disponível, então uma requisição é sempre contabilizada contra o melhor identificador que ela realmente possui.

JunoCombinando os identificadores A ordem vai do mais específico para o mais disponível, e vale a pena lê-la nesse sentido.

Uma pessoa logada é a coisa mais clara que você pode identificar. Um endereço é a mais vaga, e a única que está sempre presente.

JunoCombinando os identificadores Uma cadeia de alternativas em um único limitador tem uma falha que vale a pena conhecer: todo mundo que cai até o endereço compartilha o orçamento desse nível entre si, então o tráfego anônimo acaba competindo consigo mesmo.

Muitas vezes é melhor rodar limitadores separados, cada um com seu próprio orçamento: um mais restrito para tráfego anônimo, um mais generoso para usuários autenticados, em vez de um único limitador trocando de chave.

JunoCombinando os identificadores Coloque um prefixo na chave indicando a estratégia que a gerou: user:4821, nunca um 4821 puro. Sem isso, um id de usuário e um id de sessão podem colidir na mesma string e dois clientes sem relação nenhuma acabam compartilhando um orçamento.

A outra coisa que a cadeia esconde é que ela é um caminho de escalonamento: um atacante opera no nível que for mais barato de obter.

Então a cadeia é só tão forte quanto seu elo mais fraco, e os limites deveriam ficar mais rígidos conforme os identificadores se tornam mais anônimos, em vez de permanecerem uniformes ao longo da lista.

Coloque em prática

Uma API limita 100 requisições por minuto por endereço IP. Chegam três reclamações.

  1. Uma empresa com 400 funcionários diz que o aplicativo para de funcionar toda tarde.
  2. Um usuário no celular diz que o limite parece nunca se aplicar a ele.
  3. Alguém está fazendo scraping do catálogo público a uma taxa de aproximadamente 10.000 requisições por hora, e nada está impedindo isso.
Compare suas respostas
#O que está acontecendoCorreção
1400 pessoas compartilham um único endereço de saída, então 400 pessoas dividem 100 requisições por minutoUsar como chave o id do usuário autenticado, mantendo o limite por endereço apenas para tráfego anônimo
2A operadora move o usuário entre endereços, então cada troca dá a ele um orçamento novoA mesma correção. Uma identidade que acompanha a pessoa não pode ser descartada só por reconectar
3O catálogo é público, então não há usuário para usar como chave, e endereços custam quase nada para quem faz o scrapingLimites anônimos mais rígidos, usando como chave um prefixo IPv6 em vez de um único endereço, além de alguma proteção anterior no fluxo. Esse caso é reduzido, não resolvido

Um e dois são o mesmo defeito visto de dois ângulos diferentes: o endereço é grosseiro demais para o escritório e fino demais para o usuário móvel.

Três é o limite honesto de toda a técnica. O rate limiting faz o abuso custar alguma coisa, e contra um endpoint público esse preço nunca é maior do que o custo do seu identificador mais barato.

Para onde isso vai a seguir

O limitador conta corretamente e contra a coisa certa. O que ainda resta é o pico na fronteira da janela fixa, que deixa passar o dobro do limite em torno de um reset.

Algoritmos de janela deslizante resolve isso, medindo contra uma janela que se move junto com a requisição em vez de um bloco fixo no relógio.