APIs são a superfície de ataque primária de sistemas modernos. Enquanto aplicações web tradicionais tinham um ponto de entrada — o browser que renderiza HTML — sistemas com APIs públicas ou semi-públicas expõem centenas de endpoints que qualquer cliente programático pode acessar com qualquer dado que queira enviar. A OWASP publicou em 2019 (atualizado em 2023) o API Security Top 10 — uma lista separada do OWASP Top 10 de aplicações web, porque as vulnerabilidades de APIs têm padrões e controles distintos. Os dois primeiros itens da lista — Broken Object Level Authorization (BOLA) e Broken Authentication — são responsáveis por uma fração desproporcional dos breaches em APIs.
Este conceito cobre as vulnerabilidades mais frequentes e os controles concretos: CORS e o que o browser realmente protege (menos do que a maioria imagina), input validation com allowlist, SQL injection com exemplos nas três linguagens, mass assignment como vetor silencioso de escalada de privilégio, e IDOR/BOLA como a categoria mais explorada em APIs REST. O pano de fundo é o OWASP API Security Top 10 — a taxonomia que categoriza esses vetores.
CORS — o que o browser faz e o que o servidor controla
CORS (Cross-Origin Resource Sharing, RFC 6454 + Living Standard do W3C) é frequentemente mal entendido como "uma proteção de segurança que o servidor implementa". Na realidade, CORS é uma política do browser que relaxa a Same-Origin Policy (SOP) para permitir requisições cross-origin controladas. O servidor não precisa validar o Origin para proteger dados — ele precisa não incluir o header Access-Control-Allow-Origin para que o browser bloqueie o acesso da página ao resultado. Mas um cliente não-browser (curl, Postman, código de servidor) ignora completamente CORS.
O fluxo de uma requisição CORS:
// Cenário: SPA em app.com faz fetch para api.com
fetch('https://api.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer TOKEN' },
body: JSON.stringify({ ... })
});
// O browser detecta que é cross-origin (app.com → api.com) e envia preflight:
OPTIONS /data HTTP/1.1
Host: api.com
Origin: https://app.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
// Servidor responde ao preflight:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.com // só permite app.com, não wildcard
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 3600 // cache do preflight por 1 hora
// Agora o browser envia a requisição real e dá acesso ao código da página à resposta.
// Um cliente não-browser enviaria a requisição sem preflight e ignoraria os headers CORS.
Configurar Access-Control-Allow-Origin: * com Access-Control-Allow-Credentials: true é inválido — o browser rejeita a combinação. Mas configurar Access-Control-Allow-Origin: * sem credentials permite que qualquer origem leia a resposta. Para APIs públicas (documentação, dados abertos), wildcard pode ser correto; para APIs privadas, a lista de origens permitidas deve ser explícita e revisada. A tentação de Access-Control-Allow-Origin: * em APIs privadas "porque é mais fácil" é uma configuração de segurança incorreta.
O padrão correto para APIs que atendem múltiplas origens conhecidas é uma allowlist de origens verificadas dinamicamente:
// Go — verificação dinâmica de origin com allowlist
var allowedOrigins = map[string]bool{
"https://app.com": true,
"https://www.app.com": true,
"https://admin.app.com": true,
"http://localhost:3000": true, // apenas desenvolvimento
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Vary", "Origin") // importante para cache intermediário
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization")
w.Header().Set("Access-Control-Max-Age", "3600")
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Input validation — allowlist como defesa primária
Todo dado que entra na aplicação por fronteiras externas — body HTTP, query params, headers, path params — é entrada de usuário não confiável. Validação de input é a primeira linha de defesa: rejeitar entradas que não se conformam ao shape esperado antes de qualquer processamento.
A distinção entre allowlist (positive validation) e denylist (negative validation / blacklist) é fundamental. Denylist bloqueia padrões conhecidos de ataque: DROP TABLE, <script>, ../. O problema é que a lista de padrões maliciosos é infinita — encodings alternativos, unicode, double encoding, e combinações novas sempre existem. Allowlist define o que é válido e rejeita tudo o mais: um campo de nome aceita apenas letras (incluindo acentos), espaços e hífens — qualquer outra coisa é inválida independente de ser ou não um ataque conhecido.
public class CreateDocumentRequest
{
public string Title { get; set; }
public string Content { get; set; }
public string Category { get; set; }
}
public class CreateDocumentValidator : AbstractValidator<CreateDocumentRequest>
{
private static readonly string[] AllowedCategories = ["technical", "legal", "hr"];
public CreateDocumentValidator()
{
RuleFor(x => x.Title)
.NotEmpty()
.MaximumLength(200)
.Matches(@"^[\w\s\-\.,:!?áéíóúãõâêîôûàèìòùçÁÉÍÓÚÃÕÂÊÎÔÛÀÈÌÒÙÇ]+$") // allowlist de chars
.WithMessage("Título contém caracteres inválidos");
RuleFor(x => x.Category)
.Must(c => AllowedCategories.Contains(c))
.WithMessage($"Categoria deve ser: {string.Join(", ", AllowedCategories)}");
RuleFor(x => x.Content)
.NotEmpty()
.MaximumLength(100_000);
}
}
FluentValidation separa regras de validação do model, permitindo testes unitários da validação isolados do controller. O Matches usa allowlist de caracteres — mais seguro que denylist.
from pydantic import BaseModel, Field, field_validator
from typing import Literal
import re
TITLE_PATTERN = re.compile(r'^[\w\s\-\.,!?áéíóúãõâêîôûàèìòùçÁÉÍÓÚÃÕÂÊÎÔÛÀÈÌÒÙÇ]+$')
class CreateDocumentRequest(BaseModel):
title: str = Field(min_length=1, max_length=200)
content: str = Field(min_length=1, max_length=100_000)
category: Literal["technical", "legal", "hr"] # allowlist via Literal
@field_validator("title")
@classmethod
def title_chars(cls, v: str) -> str:
if not TITLE_PATTERN.match(v):
raise ValueError("Título contém caracteres inválidos")
return v
# FastAPI integra Pydantic automaticamente — validação antes do handler
@router.post("/documents")
async def create_document(body: CreateDocumentRequest, ...):
# body.title, body.content, body.category já são válidos aqui
...
Pydantic v2 usa Literal para allowlist de valores — mais expressivo que validators manuais para enums. O FastAPI rejeita requests inválidas com 422 automaticamente antes de chamar o handler.
import "github.com/go-playground/validator/v10"
type CreateDocumentRequest struct {
Title string `json:"title" validate:"required,min=1,max=200,title_chars"`
Content string `json:"content" validate:"required,min=1,max=100000"`
Category string `json:"category" validate:"required,oneof=technical legal hr"`
}
var titleRegex = regexp.MustCompile(`^[\w\s\-\.,!?áéíóúãõâêîôûàèìòùç]+$`)
func init() {
validate.RegisterValidation("title_chars", func(fl validator.FieldLevel) bool {
return titleRegex.MatchString(fl.Field().String())
})
}
func CreateDocument(w http.ResponseWriter, r *http.Request) {
var req CreateDocumentRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", 400); return
}
if err := validate.Struct(req); err != nil {
http.Error(w, err.Error(), 422); return
}
// req é válido aqui
}
oneof implementa allowlist de valores nativamente. Validators customizados com regex implementam allowlist de caracteres. Nunca confiar em dados recebidos sem validação — nem de outros serviços internos.
Mass assignment — um vetor silencioso de escalada de privilégio
Mass assignment ocorre quando o servidor aceita e aplica ao model qualquer propriedade presente no request body, sem filtrar. Em frameworks que fazem binding automático de JSON para objetos (Entity Framework, SQLAlchemy, GORM), se o request body for mapeado diretamente para o model de banco de dados, o cliente pode incluir propriedades que não deveria poder alterar.
// Vulnerável — binding direto do request para o model de banco
// O cliente pode enviar {"title": "...", "userId": 999, "isAdmin": true, "price": 0}
[HttpPut("/documents/{id}")]
public async Task<IActionResult> Update(int id, [FromBody] Document document)
{
document.Id = id;
await _db.Documents.UpdateAsync(document); // persiste tudo incluindo userId e isAdmin
return Ok();
}
// Correto — DTO separado expõe apenas o que o cliente pode alterar
public class UpdateDocumentDto
{
public string Title { get; set; }
public string Content { get; set; }
// userId, isAdmin, price NÃO estão no DTO
}
[HttpPut("/documents/{id}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateDocumentDto dto)
{
var doc = await _db.Documents.FindAsync(id);
if (doc == null) return NotFound();
if (doc.UserId != User.GetUserId()) return Forbid(); // authorization check
doc.Title = dto.Title; // apenas os campos permitidos
doc.Content = dto.Content;
await _db.SaveChangesAsync();
return Ok();
}
O padrão correto é sempre ter DTOs (Data Transfer Objects) distintos dos modelos de banco de dados, com apenas as propriedades que o cliente tem permissão de alterar. Nunca bind o request body diretamente para um model de banco. Em Python com SQLAlchemy e FastAPI, isso significa usar schemas Pydantic distintos para create/update separados do model SQLAlchemy. Em Go com GORM, usar structs de request separadas das structs de model.
IDOR e BOLA — Broken Object Level Authorization
IDOR (Insecure Direct Object Reference) — o primeiro item do OWASP API Security Top 10 como BOLA (Broken Object Level Authorization) — é a vulnerabilidade mais comum em APIs REST. O problema: o servidor autentica o usuário (verifica JWT, identifica quem é) mas não verifica se o usuário tem autorização para acessar o objeto específico referenciado pelo ID na URL.
# Vulnerável — autentica mas não autoriza por objeto
GET /api/v1/documents/4821
Authorization: Bearer TOKEN_DO_USUARIO_A
# O servidor verifica o JWT (usuário A está autenticado)
# Mas busca o documento 4821 sem verificar se pertence ao usuário A
# Se o documento 4821 pertence ao usuário B, usuário A o recebe
# Correto — verificação de ownership
SELECT * FROM documents WHERE id = $1 AND user_id = $2
-- $1 = 4821 (da URL), $2 = user_id do JWT (não do body)
A variante mais silenciosa de IDOR é quando os IDs são não-sequenciais (UUIDs) — o desenvolvedor assume que se o ID não é adivinhável, não há IDOR. O problema é que há muitas fontes de vazamento de IDs: logs da aplicação, respostas de erro que incluem IDs relacionados, listagens que incluem IDs de outros usuários, e referências em URLs compartilhadas. Security by obscurity não é controle de autorização.
IDOR também aparece em operações de mutação: DELETE /documents/4821 de um usuário não-autorizado, PUT /users/999/admin para escalar privilégio. A verificação precisa acontecer em cada operação — não apenas em leituras.
A regra de ouro contra IDOR: o identificador do objeto na URL é o qual; o identificador do usuário no JWT é o quem. Qualquer query de banco de dados que use o "qual" sem o "quem" é candidata a IDOR. A exceção são recursos genuinamente públicos — mas esses devem ser explicitamente marcados, não a regra default.
Rate limiting — prevenção de abuso e brute force
Rate limiting em APIs previne abuso: brute force de credenciais, enumeração de IDs, scraping de dados, e DoS por volume de requisições. Os algoritmos de rate limiting (token bucket, leaky bucket, sliding window) foram cobertos em profundidade no módulo 09. Para segurança de APIs, o que importa é a granularidade:
- Por IP: mais simples, mas contornável por botnets com muitos IPs. Necessário mas não suficiente.
- Por conta: mais efetivo para brute force de senha específico a uma conta — um atacante com mil IPs ainda enfrenta o limite por conta.
- Por endpoint: endpoints sensíveis (login, forgot-password, criar conta, transferir dinheiro) têm limites menores que endpoints normais.
- Por API key: para APIs com chaves de cliente, o limite por chave impede um cliente abusivo de afetar outros.
O header de resposta para rate limit deve comunicar o estado ao cliente: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, e Retry-After quando o limite é atingido (429 Too Many Requests). Isso permite que clientes bem-comportados adaptem seu ritmo de chamadas.
Exposição excessiva de dados — responder apenas o necessário
O item 3 do OWASP API Security Top 10 (2023) é Broken Object Property Level Authorization — APIs que retornam mais dados do que o cliente deveria ver. O padrão mais comum: o endpoint retorna o model completo (incluindo campos internos, dados de outros usuários, ou informações sensíveis) esperando que o frontend filtre o que exibe. O problema é que clientes diferentes (mobile, web, parceiros externos) podem usar a mesma API, e campos não exibidos na UI ainda são transmitidos e armazenados no cliente.
// Problemático — retorna o model completo do banco
type User struct {
ID int // ok
Email string // ok para o próprio usuário; não para outros
PasswordHash string // NUNCA deve ir para o cliente
InternalNotes string // apenas admin
CreatedAt time.Time // ok
LastLoginIP string // PII — só para o próprio usuário
IsAdmin bool // não expor se não necessário para o cliente
}
// Correto — projeção específica por contexto
type UserPublicView struct {
ID int `json:"id"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type UserSelfView struct { // para o próprio usuário
ID int `json:"id"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
LastLoginIP string `json:"last_login_ip"` // visível apenas para si
}
OWASP API Security Top 10 2023 — panorama completo
Para referência, os dez itens da lista mais recente com mapeamento para os controles cobertos neste módulo:
- BOLA (Broken Object Level Authorization) — IDOR: verificação de ownership em toda operação.
- Broken Authentication — coberto no conceito 03 e 04: JWT correto, rate limit no login, MFA.
- Broken Object Property Level Authorization — projeções por contexto, DTOs distintos, mass assignment prevention.
- Unrestricted Resource Consumption — rate limiting, paginação obrigatória, limites de tamanho de upload, query complexity limits em GraphQL.
- Broken Function Level Authorization — endpoints administrativos verificam papel admin, não apenas autenticação.
- Unrestricted Access to Sensitive Business Flows — bot detection, CAPTCHA em fluxos críticos, monitoramento de padrões anômalos.
- Server-Side Request Forgery (SSRF) — coberto no conceito 02: allowlist de URLs, não aceitar IPs internos.
- Security Misconfiguration — headers de segurança, TLS (conceito 08), configuração de CORS restritiva.
- Improper Inventory Management — não deixar APIs legadas expostas, documentar e versionar endpoints.
- Unsafe Consumption of APIs — validar respostas de APIs de terceiros, não confiar em dados externos.
Decisões de engenharia
Wildcard (*): adequado apenas para APIs genuinamente públicas — dados abertos, CDN de assets, documentação. Não pode ser usado com Allow-Credentials: true. Qualquer página pode ler a resposta.
Allowlist dinâmica: padrão para APIs privadas e semi-públicas. Verificar o header Origin contra uma lista configurada; retornar o Origin exato se autorizado (nunca wildcardificar domínios por prefixo/sufixo). Adicionar Vary: Origin para caches intermediários não servirem resposta de origem A para origem B.
Null origin: nunca permitir Access-Control-Allow-Origin: null — pode ser enviado por iframes sandboxed usados em ataques de CSRF.
Sempre usar allowlist (positive validation): definir o que é válido (caracteres permitidos, tamanho, formato, valores aceitos) e rejeitar tudo o mais. Um campo de nome aceita [\w\s\-áéíóú...] — qualquer coisa fora disso é inválida, independente de ser ou não um ataque.
Nunca depender apenas de denylist: a lista de padrões maliciosos é infinita — encodings alternativos, variantes unicode, double encoding, e ataques futuros desconhecidos. Denylist pode ser uma camada adicional (WAF), mas não a única.
Canonicalizar antes de validar: decodificar URL encoding, normalizar unicode (NFC), resolver path traversal antes de aplicar a allowlist — atacantes exploram a diferença entre o que a allowlist vê e o que o código usa.
Padrão de query: toda query que usa um ID da URL deve incluir o user_id do JWT como condição adicional: WHERE id = $1 AND user_id = $2. Se o resultado for vazio, retornar 404 (não 403) para não confirmar existência do recurso.
Recursos compartilhados: quando múltiplos usuários podem ter acesso (documentos compartilhados, projetos em equipe), usar tabela de permissões explícita: WHERE id = $1 AND EXISTS (SELECT 1 FROM permissions WHERE resource_id = $1 AND user_id = $2).
UUIDs não eliminam IDOR: são menos adivinháveis, mas IDs vazam por logs, erros, listagens, e URLs compartilhadas. Tratá-los como token de autorização é security by obscurity.
DTOs separados (preferido): ter structs/classes distintas para request e model de banco. O DTO expõe apenas os campos que o cliente pode alterar — sem campo IsAdmin, sem campo UserId. A conversão DTO → model é explícita e auditável.
Annotations de exclusão ([JsonIgnore], exclude=True): mais frágil — um novo campo adicionado ao model é automaticamente incluído na deserialização até alguém lembrar de excluí-lo. O modelo mental de "exclusão" é mais difícil de auditar que "inclusão explícita".
Regra de design: o schema de request deve ser tão pequeno quanto possível — incluir apenas o que o cliente precisa enviar para aquela operação, não todo o modelo de dados.
Como praticar
-
Testar IDOR sistematicamente em uma API local com dois usuários. Em qualquer API que você controla com recursos por usuário, crie dois usuários de teste (A e B). Com o usuário A autenticado, tente acessar recursos do usuário B variando o ID na URL:
GET /documents/ID_DE_B,PUT /documents/ID_DE_B,DELETE /documents/ID_DE_B. Documente cada endpoint que não verifica ownership. Implemente a correção (WHERE id = $1 AND user_id = $2) e re-execute os testes.
Critério: todos os endpoints de acesso a recursos por ID retornam 404 (não 200 nem 403) quando o recurso pertence a outro usuário; testes automatizados cobrem as três operações (read, update, delete) para cada tipo de recurso; zero false negatives — nenhum endpoint que deveria verificar ownership deixa de verificar. -
Auditar e corrigir a configuração CORS de uma API existente. Use
curl -H "Origin: https://evil.com" -I https://sua-api.com/endpointpara verificar quais origens o servidor aceita. Tente origens malformadas: subdomínio com prefixo (evil-app.comvsapp.com),null, e variações case-insensitive. Compare com a allowlist intencional. Implemente a allowlist dinâmica comVary: Origine verifique que origens não autorizadas recebem resposta sem o header CORS.
Critério: curl comOrigin: https://evil.comnão recebeAccess-Control-Allow-Originem nenhum endpoint privado; o headerVary: Originestá presente em todas as respostas com CORS; origens de desenvolvimento (localhost) não estão habilitadas em produção. -
Implementar schema de validação rigorosa com allowlist para um endpoint existente. Escolha um endpoint de criação de recurso. Escreva um schema de validação que usa: allowlist de caracteres para strings livres (regex), allowlist de valores para campos enum, limites de tamanho explícitos, e tipos corretos para todos os campos. Teste com dados válidos, inválidos, e payloads que incluem campos extras não declarados no schema.
Critério: campos extras no body são ignorados ou rejeitados com 422 (nunca silenciosamente persistidos); caracteres fora da allowlist resultam em 422 com mensagem descritiva indicando qual campo falhou; a validação é testada com ao menos 5 payloads inválidos distintos — cada um cobrindo uma regra diferente. -
Refatorar um endpoint vulnerável a mass assignment para usar DTOs explícitos. Encontre um endpoint que faz binding direto do request body para um model de banco (ou simule o padrão). Identifique todos os campos do model que o cliente não deveria poder alterar. Crie um DTO com apenas os campos permitidos e refatore o handler para usar o DTO. Teste que enviar campos não-declarados no DTO (como
isAdmin: true) não os altera no banco.
Critério: o DTO tem exatamente e apenas os campos que o cliente precisa enviar; um test de integração verifica que a tentativa de alterar campos protegidos (isAdmin, userId, createdAt) não tem efeito; a conversão DTO → model é explícita (campo a campo), não via reflection ou merge automático. -
Implementar rate limiting por conta em endpoint de login com detecção de brute force. Adicione rate limiting ao endpoint de autenticação com dois níveis: por IP (limite maior, ex: 30 req/min) e por conta (limite menor, ex: 5 tentativas em 15 min). Após 5 falhas para a mesma conta, retornar 429 com
Retry-After. Implemente exponential backoff: o delay aumenta a cada tentativa falha consecutiva (1s, 2s, 4s, 8s...). Adicione headerX-RateLimit-Remainingnas respostas.
Critério: após 5 tentativas com senha errada para a mesma conta, o endpoint retorna 429 por 15 min independente do IP; o limit por IP protege contra tentativas distribuídas; logins bem-sucedidos resetam o contador de falhas da conta; os headers de rate limit comunicam o estado ao cliente.
Perguntas de entrevista
O que é CORS e por que não é uma proteção de segurança do servidor — o que ele realmente protege?
CORS (Cross-Origin Resource Sharing) é uma política do browser que relaxa a Same-Origin Policy para permitir que JavaScript em uma origem (ex: app.com) faça requisições para outra origem (ex: api.com) de forma controlada. O servidor não implementa CORS para se proteger — ele implementa CORS para autorizar browsers a acessar a resposta em nome de origens específicas.
O que CORS protege: impede que uma página maliciosa (evil.com) faça requisições autenticadas para api.com usando os cookies do usuário e leia a resposta. Sem CORS, um site malicioso poderia fazer fetch('https://api.com/conta', {credentials: 'include'}) e ler o saldo bancário do usuário logado.
O que CORS NÃO protege: qualquer cliente não-browser ignora completamente os headers CORS. Curl, Postman, Python requests, código de servidor — nenhum deles verifica ou respeita CORS. A proteção é exclusivamente para JavaScript executando no browser. Portanto, uma API que depende de CORS como única proteção contra acesso não autorizado está fundamentalmente incorreta — a API precisa de autenticação e autorização próprias independentemente da configuração CORS.
Como defender uma API contra IDOR/BOLA sistematicamente? O que significa "UUID não elimina IDOR"?
A defesa sistemática contra IDOR tem dois pilares: (1) toda query de banco que usa um ID da URL deve incluir o user_id do JWT como condição obrigatória — WHERE id = $1 AND user_id = $2 — tornando impossível acessar recursos de outros usuários mesmo com o ID correto; (2) o padrão de resposta deve ser 404 (não 403) quando o recurso não pertence ao usuário, para não confirmar a existência do objeto.
"UUID não elimina IDOR" porque a segurança por obscuridade não é controle de autorização. UUIDs são menos adivinháveis que IDs sequenciais, mas há muitas fontes de vazamento: logs de aplicação (qualquer log que inclua o UUID de um recurso de outro usuário), respostas de erro detalhadas, listagens que incluem IDs de recursos de outros usuários, URLs compartilhadas, e APIs de busca que retornam IDs. Um atacante com acesso a qualquer dessas fontes pode usar UUIDs de outros usuários — e sem verificação de ownership, a API os aceita. O controle correto é verificar ownership em toda operação, independente do formato do ID.
O que é mass assignment, por que é perigoso, e qual o padrão correto de mitigação?
Mass assignment ocorre quando o servidor mapeia automaticamente todos os campos do request body para um model — incluindo campos que o cliente não deveria poder alterar. Em frameworks como Entity Framework, Rails (com permit incorreto), ou qualquer ORM com binding automático, um campo como isAdmin ou userId pode ser alterado pelo cliente simplesmente incluindo-o no JSON do request.
Por que é perigoso: um usuário comum pode se tornar admin enviando {"name": "Alice", "isAdmin": true} no endpoint de atualização de perfil. O campo não aparece na UI, mas a API aceita e persiste qualquer campo presente no body que tenha binding no model. O ataque é silencioso — não há erro, a operação "funciona", e a escalada de privilégio pode não ser detectada por semanas.
Mitigação padrão: DTOs explícitos com apenas os campos que o cliente tem permissão de alterar. O DTO de UpdateProfile tem name e bio; não tem isAdmin, userId, createdAt. A conversão DTO → model é feita campo a campo — explícita e auditável. Em Python com Pydantic: um schema diferente para cada operação (CreateUser, UpdateUser, UserResponse). Em Go: structs de request separadas das structs de model. A regra: nunca faça binding do request body diretamente para um model de banco.
Por que allowlist é superiora a denylist para input validation? Como implementar allowlist corretamente?
Denylist (blacklist) bloqueia padrões conhecidos de ataque: DROP TABLE, <script>, ../../../. O problema fundamental é que a lista de padrões maliciosos é infinita e em constante evolução. Atacantes exploram encodings alternativos (%3Cscript%3E, <script>, unicode homoglyphs), double encoding, variações de whitespace, e combinações de payloads que a denylist não reconhece. Uma nova técnica de evasão torna toda a denylist ineficaz até ser atualizada.
Allowlist (positive validation) define o que é válido e rejeita tudo o mais. Um campo de nome aceita ^[\w\s\-áéíóú]+$ — letras, espaços, hífens e acentos. Qualquer caractere fora desse conjunto é inválido, independente de ser um ataque ou não. A propriedade chave: um novo vetor de ataque que usa caracteres fora da allowlist é automaticamente bloqueado sem atualização da regra.
Implementação correta: (1) canonicalizar o input antes de validar — decodificar URL encoding, normalizar unicode para NFC, resolver sequências de escape — para que a allowlist veja o mesmo que o código de negócio vai processar; (2) definir a allowlist o mais restrita possível para o caso de uso (um campo de CPF aceita apenas dígitos + pontuação específica); (3) retornar erro 422 com indicação de qual campo e qual regra falhou — não expor a regex interna, mas ser descritivo o suficiente para o cliente corrigir o input legítimo.
Quais são os itens mais críticos do OWASP API Security Top 10 2023 e como se diferenciam do OWASP Top 10 tradicional?
O OWASP API Security Top 10 surgiu porque APIs têm vetores de ataque distintos das aplicações web tradicionais. Os mais críticos:
BOLA (Broken Object Level Authorization): o item #1, responsável por uma fração desproporcional de breaches. APIs REST usam IDs em URLs para identificar recursos — sem verificação de ownership, qualquer usuário autenticado pode acessar recursos de outros. O OWASP Top 10 tradicional tem BFLA (function level), mas BOLA é específico de objetos e muito mais prevalente em APIs.
Broken Object Property Level Authorization (#3): APIs retornam mais campos do que o cliente deveria ver, e aceitam mais campos do que o cliente deveria poder alterar (mass assignment). Não existe equivalente claro no OWASP Top 10 web.
Unrestricted Resource Consumption (#4): APIs sem paginação obrigatória, sem limite de tamanho de upload, sem query complexity limits (GraphQL) são vulneráveis a DoS por volume de dados — não por volume de requisições. Diferente de DoS tradicional.
A diferença fundamental do Top 10 web: o OWASP Web Top 10 foca em vulnerabilidades de aplicações com frontend (XSS, CSRF, injection em formulários). O API Top 10 foca em problemas de autorização granular, exposição de dados, e consumo de recursos — específicos de sistemas com API diretamente consumida por clientes programáticos.
Referências para aprofundar
- docs OWASP API Security Top 10 — 2023 — OWASP Foundation.
- book Hacking APIs — Corey Ball (No Starch, 2022).
- docs OWASP Mass Assignment Prevention Cheat Sheet.
- docs Fetch Living Standard — CORS — WHATWG.
- article CORS in Detail — MDN Web Docs.
- article IDOR Vulnerabilities in the Wild — HackerOne.
- book API Security in Action — Neil Madden (Manning, 2020).
- article Exploiting CORS Misconfigurations for Bitcoins and Bounties — James Kettle (Portswigger, 2016).
- vídeo OWASP API Security Top 10 Deep Dive — 42Crunch (YouTube, 2023).
- docs GraphQL Security — OWASP Cheat Sheet.
- article Input Validation — Defense In Depth — SANS Institute.
- article Rate Limiting Strategies and Techniques — Stripe Engineering Blog.