Uma API que passou nos testes funcionais ainda pode estar aberta o suficiente para ser explorada no primeiro dia em produção. A verificação de segurança é uma etapa própria, com critérios que não se confundem com “funciona”: autenticação correta, autorização por recurso, limites de uso e controle do que a resposta devolve. Este texto lista o que precisa estar fechado antes de expor uma API REST na internet e onde as decisões costumam ser tomadas errado.
Autenticação e autorização são duas verificações separadas
Confirmar quem faz a chamada e confirmar que esse alguém pode acessar aquele recurso são checagens independentes. A maior parte das falhas graves de API não está na autenticação — o token é válido — e sim na autorização: o endpoint devolve o pedido de número 4821 sem verificar se ele pertence ao usuário autenticado. Isso é o clássico Broken Object Level Authorization, o item que lidera as listas de vulnerabilidades de API há anos.
A regra prática: toda rota que recebe um identificador de recurso precisa validar propriedade no servidor, nunca confiar que o cliente só vai pedir o que é dele. Não adianta esconder o botão no front-end. Se o recurso é acessível por GET /pedidos/4821, alguém vai trocar o número na mão.
@app.get("/pedidos/{pedido_id}")
def obter_pedido(pedido_id: int, usuario = Depends(auth)):
pedido = repo.buscar(pedido_id)
if pedido is None or pedido.dono_id != usuario.id:
raise HTTPException(status_code=404) # 404, nao 403
return pedido
Devolver 404 em vez de 403 quando o recurso existe mas não pertence ao usuário evita confirmar a existência do objeto para quem não deveria enxergá-lo. É uma escolha deliberada, não descuido. A autorização por atributo de nível de campo é o passo seguinte que muita API pula: o mesmo endpoint pode ser legítimo para dois papéis, mas um administrador enxerga o campo de custo interno e o cliente final não. Filtrar isso na saída, por papel, evita que a permissão de rota vire permissão de dado.
Escolha do mecanismo de token e sua revogação
A decisão entre JWT autocontido e token opaco com consulta no servidor tem um trade-off que decide o resto da arquitetura de sessão. O JWT dispensa consulta a cada requisição, o que escala bem, mas não há como invalidá-lo antes de expirar sem reintroduzir estado — uma blocklist ou uma verificação de versão. O token opaco custa uma consulta por chamada, geralmente a um cache em memória, e em troca a revogação é imediata.
| Critério | JWT autocontido | Token opaco + store |
|---|---|---|
| Custo por requisição | Verificação de assinatura | Consulta ao store |
| Revogação imediata | Exige estado extra | Nativa |
| Vazamento de payload | Dados legíveis no token | Nada exposto |
| Melhor cenário | Muitos serviços, expiração curta | Sessão que precisa de logout real |
Para a maioria dos sistemas com login de usuário e necessidade de “encerrar sessão agora”, o token opaco com store de baixa latência é a escolha mais segura. JWT com expiração de 15 minutos e refresh rotativo resolve o caso de arquitetura distribuída onde a consulta central seria gargalo. O erro comum é adotar JWT longo por conveniência e descobrir tarde que não há como derrubar um token comprometido.
Comunicação entre serviços internos é um caso à parte: token de usuário não deve trafegar de serviço para serviço como se fosse credencial de máquina. Aqui entram mTLS ou credenciais de cliente de curta duração, com escopo restrito ao que aquele serviço precisa chamar. Reaproveitar o token do usuário em toda a cadeia de chamadas amplia o estrago de um único vazamento para o sistema inteiro.
Rate limiting e proteção contra abuso
Sem limite de taxa, um endpoint de autenticação vira alvo de força bruta e qualquer rota cara vira vetor de negação de serviço. O limite precisa existir em duas camadas: uma global por IP na borda e uma por identidade autenticada na aplicação, com janelas diferentes para rotas sensíveis. Login e recuperação de senha pedem limites bem mais rígidos que uma leitura de catálogo.
INCR chave na primeira chamada, EXPIRE chave 60 junto, e recusa quando o contador passa do teto. Chave no formato rl:{rota}:{user_id} para separar o orçamento de cada rota por usuário, em vez de um balde único que a leitura esvazia antes de o login precisar.Retorne 429 com o cabeçalho Retry-After para que clientes legítimos saibam recuar. E registre o estouro: uma sequência de 429 em recuperação de senha vindo de um mesmo intervalo de IPs é sinal de ataque em andamento, não de usuário distraído. Em ambiente com várias instâncias da API, o contador precisa ser compartilhado — um limitador em memória local vaza porque cada réplica conta o seu próprio balde e o teto efetivo vira o teto multiplicado pelo número de instâncias.
Validação de entrada e a superfície de injeção
Toda entrada é hostil até prova em contrário. Isso vale para o corpo da requisição, parâmetros de query, cabeçalhos e o próprio caminho. A validação por esquema — tamanho máximo, tipo, formato, faixa de valores — precisa acontecer antes de a lógica de negócio tocar o dado. Bibliotecas de validação declarativa resolvem o grosso, desde que o esquema recuse campos desconhecidos em vez de ignorá-los.
Consultas ao banco sempre parametrizadas, sem concatenar string de query. Isso elimina a injeção de SQL clássica, mas a mesma disciplina vale para qualquer interpretador que a entrada alcance: comandos de shell, montagem de filtros NoSQL, expressões passadas a um motor de template. Um teto explícito no tamanho do corpo aceito — configurado no servidor de aplicação, não só no proxy — corta o envio de payloads gigantes projetados para estourar memória.
Um vetor que passa despercebido é o SSRF: qualquer campo em que o usuário fornece uma URL que o servidor vai buscar — webhook, importação por link, geração de miniatura — pode ser apontado para endereços internos da rede, incluindo metadados de nuvem. A defesa é uma lista de permissão de destinos e o bloqueio explícito de faixas privadas e do endereço de metadados, resolvido depois da resolução de DNS para impedir rebind.
O que a resposta devolve sem querer
Um vazamento comum não está no que a API protege, e sim no que ela responde. Serializar o objeto inteiro do banco expõe campos que ninguém pediu: hash de senha, flags internas, dados de outro contexto. A resposta precisa ser modelada por um esquema de saída explícito, com os campos permitidos listados um a um, em vez de despejar o modelo de persistência direto no JSON.
Mensagens de erro merecem o mesmo cuidado. Um stack trace devolvido ao cliente entrega versão de framework, estrutura de diretório e às vezes trecho de query. Em produção, o erro que chega ao cliente é genérico e correlacionado a um identificador; o detalhe fica no log interno. Cabeçalhos que anunciam a stack — X-Powered-By, Server com versão — saem por padrão.
Segredos, dependências e o checklist de corte
Chave de API, credencial de banco e segredo de assinatura não moram no código nem em variável commitada. Ficam em um cofre de segredos ou em variável de ambiente injetada no deploy, com rotação prevista. E o log, que é onde muita coisa escapa: senha, token e número de cartão não podem ser gravados, o que exige uma camada de redação antes da escrita, não confiança em quem chama o logger.
A cadeia de dependências é superfície de ataque tão real quanto o código próprio. Uma verificação automatizada de vulnerabilidades conhecidas nos pacotes, rodando no pipeline e falhando o build quando aparece falha crítica, evita subir para produção com uma biblioteca já sabidamente explorável. Fixar versões e revisar o que entra por transitividade fecha a porta do comprometimento por dependência substituída.
Antes do deploy, a lista mínima que precisa estar toda marcada: HTTPS obrigatório com redirecionamento de HTTP; CORS restrito a origens conhecidas, sem * em rota autenticada; autorização por recurso testada com um usuário tentando acessar dado de outro; rate limit ativo em login e recuperação; esquema de saída que não vaza campo interno; erros genéricos ao cliente; segredos fora do repositório; dependências sem vulnerabilidade conhecida na verificação automatizada. Nenhum desses itens é opcional para uma API pública, e o barato de fechá-los antes é sempre menor que o custo de responder a um incidente depois que o tráfego real chegou.
Deixe um comentário