Segurança e identidade

OWASP Top 10: as dez categorias de risco e como corrigir cada uma

Intermediário9 min de leituraAtualizado

Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês

O OWASP Top 10 é um documento de conscientização que identifica as categorias de risco responsáveis pela maior parte dos comprometimentos de aplicações web. Este guia percorre cada categoria com um exemplo realista da falha e a correção específica em uma stack Java e web, e depois explica como usar a lista como ponto de partida, e não como checklist, e em que momento o Application Security Verification Standard assume.

O que você vai aprender

  • Reconhecer cada categoria do OWASP Top 10 em código de aplicações reais
  • Aplicar a mitigação concreta contra quebra de controle de acesso, injeção e falhas criptográficas
  • Diferenciar design inseguro de bugs de implementação e tratar cada um da forma adequada
  • Gerenciar o risco de dependências e da cadeia de suprimentos com um SBOM e builds reproduzíveis
  • Usar o ASVS para transformar conscientização em requisitos de segurança testáveis

O OWASP Top 10 é o documento mais citado em segurança de aplicações e também o mais mal utilizado. É um ranking, revisado periodicamente e baseado em dados, das categorias de fraquezas que aparecem com mais frequência em aplicações reais — uma forma de direcionar a atenção, escrita para conscientizar, não para certificar. Lido dessa forma, ele tem valor real: as categorias são amplas o bastante para sobreviver às modas de frameworks, e cada uma nomeia uma classe de erro que já custou muito caro às organizações.

A seguir está cada categoria, com uma falha concreta do tipo que aparece em código comum e o controle específico que a elimina. A abordagem é sempre defensiva: o suficiente sobre como a fraqueza é explorada para reconhecê-la em code review, acompanhado da correção. Quando um tema merece mais profundidade do que um resumo, há um link para seguir adiante — a visão arquitetural está em arquitetura de segurança de aplicações, e o tratamento de tokens específico de APIs em protegendo APIs REST com JWT.

Quebra de controle de acesso

O eterno número um: um usuário autenticado acessando dados ou operações que pertencem a outra pessoa. O formato clássico é um identificador tirado direto da requisição e usado para buscar um registro, sem nenhuma verificação de que o registro pertence a quem fez a chamada.

// Vulnerável: qualquer usuário autenticado pode ler qualquer fatura trocando o número
@GetMapping("/invoices/{id}")
public Invoice get(@PathVariable long id) {
    return invoices.findById(id).orElseThrow(NotFoundException::new);
}

// Corrigido: a consulta fica restrita ao principal autenticado
@GetMapping("/invoices/{id}")
public Invoice get(@PathVariable long id, Authentication auth) {
    return invoices.findByIdAndCustomer(id, auth.getName())
                   .orElseThrow(NotFoundException::new);   // 404 em vez de 403: não revelar nada
}

Variantes relacionadas: escalonamento de privilégios por uma rota de administração sem proteção, um identificador de papel ou de tenant aceito a partir de um parâmetro da requisição, forced browsing para um endpoint que a interface nunca referencia e directory traversal em um download de arquivo. Os controles são estruturais — negar por padrão, para que uma rota nova fique protegida até ser liberada, restringir toda consulta ao principal, derivar o tenant do token e não do payload, e escrever um teste por endpoint garantindo que o identificador de outro usuário retorna 404. Regras no nível de método no serviço, como mostrado em fundamentos do Spring Security, pegam o que os padrões de URL deixam passar.

Falhas criptográficas

Esta categoria trata de dados sensíveis que não estão protegidos como deveriam: transmitidos em texto claro, armazenados sem criptografia ou protegidos por algo que só parece criptografia. Os achados típicos são um serviço interno que ainda aceita HTTP sem TLS, backups de banco de dados gravados sem criptografia em um armazenamento compartilhado, dados de cartão ou de saúde guardados muito depois de deixarem de ser necessários, senhas armazenadas com um hash rápido e uma “criptografia” caseira feita com XOR ou base64.

Para corrigir, classifique os dados primeiro — não dá para proteger o que você não identificou — e depois exija TLS 1.3 em todo lugar, inclusive dentro da rede, criptografe dados em repouso com chaves guardadas em um serviço gerenciado de chaves, faça o hash das senhas com uma função memory-hard e use criptografia autenticada, como AES-GCM com um nonce único por mensagem, em vez de usar cifras de bloco diretamente. Apague o que você não precisa mais: o dado que você não guarda não vaza. As primitivas em si são explicadas em fundamentos de segurança da informação.

Injeção

Há injeção sempre que uma entrada não confiável é concatenada em algo que um interpretador analisa — SQL, um comando do sistema operacional, filtros LDAP, XPath, expressões de template. O caso do banco de dados continua sendo o mais danoso.

// Vulnerável: a entrada passa a fazer parte da instrução
String sql = "SELECT * FROM users WHERE email = '" + email + "'";

// Corrigido: o valor nunca consegue mudar a estrutura da instrução
String sql = "SELECT id, email, display_name FROM users WHERE email = ?";
jdbcTemplate.query(sql, ps -> ps.setString(1, email), USER_MAPPER);

Instruções parametrizadas são a resposta sempre que o interpretador der suporte a elas; as práticas em torno delas são abordadas em boas práticas de SQL. Quando a estrutura realmente varia — uma coluna de ordenação, um nome de tabela —, valide contra uma allow-list de identificadores conhecidos em vez de fazer escape. Para chamadas ao sistema operacional, passe um array de argumentos em vez de uma string de shell. Cross-site scripting também entra aqui: o interpretador é o navegador, e a correção é a codificação de saída contextual feita pela engine de templates, somada a uma Content Security Policy que proíba scripts inline como segunda camada.

Design inseguro

Esta categoria existe para separar falhas de design de falhas de código. Uma redefinição de senha que verifica a identidade com perguntas cuja resposta um atacante consegue pesquisar, um fluxo de reservas sem limite de quantos assentos uma sessão pode segurar, um processo de reembolso sem uma segunda aprovação — tudo isso pode ser implementado sem nenhum defeito e ainda assim ser explorável, porque o próprio mecanismo é inadequado.

Nada que se encontre em code review resolve isso. A resposta é modelagem de ameaças antes da implementação, casos de abuso escritos ao lado das user stories, arquiteturas de referência para problemas recorrentes como autenticação e pagamento, e limites projetados dentro dos fluxos em vez de adicionados depois de um incidente. Trate “está funcionando conforme especificado” como um achado quando o problema é a própria especificação.

Configuração incorreta de segurança

A causa mais comum de exposição no mundo real, e a mais fácil de corrigir: credenciais administrativas padrão ainda ativas, páginas de erro detalhadas mostrando stack traces em produção, listagem de diretórios habilitada, um endpoint de actuator ou de gerenciamento acessível sem autenticação, uma política de CORS permissiva demais, armazenamento em nuvem aberto para a internet, cabeçalhos de segurança ausentes.

# Perfil de produção: expor o mínimo e não dizer nada útil a um atacante
server:
  error:
    include-stacktrace: "never"
    include-message: "never"
management:
  endpoints:
    web:
      exposure:
        include: "health,info"
  endpoint:
    health:
      show-details: "when-authorized"

Faça o hardening construindo os ambientes a partir de código, para que a configuração possa ser revisada e seja idêntica em todo lugar, removendo aplicações de exemplo e funcionalidades não utilizadas, rodando um scan de configuração no pipeline e definindo os cabeçalhos de resposta padrão — Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options e um Referrer-Policy restritivo. Automatize: um checklist aplicado à mão se desvia em menos de um mês.

Componentes vulneráveis e desatualizados

Aplicações modernas são, em sua maior parte, código de outras pessoas, e uma vulnerabilidade conhecida em uma biblioteca amplamente usada passa a ser procurada em escala de internet poucas horas depois de divulgada. O risco não tem nada de exótico: é um framework três versões major atrás, uma dependência transitiva que ninguém sabia que existia ou uma imagem base que não é reconstruída há um ano.

Gere um SBOM no momento do build, escaneie cada commit contra uma base de alertas de segurança e automatize as atualizações de rotina para que as pessoas só cuidem das difíceis. Prefira bibliotecas com histórico de manutenção ativa, remova dependências que você deixou de usar e reconstrua as imagens de contêiner regularmente para que os pacotes do sistema operacional se mantenham atualizados. A integração contínua é o lugar certo para tudo isso — veja integração contínua para saber como manter essas verificações rápidas o bastante para que ninguém as desative.

Falhas de identificação e autenticação

Fraquezas na hora de provar quem alguém é: credential stuffing contra um login sem rate limiting, identificadores de sessão que sobrevivem a uma mudança de privilégio, tokens que nunca expiram, um link de redefinição de senha válido por dias e reutilizável, autenticação multifator que pode ser contornada chamando um endpoint legado.

Os controles são bem conhecidos — verificar as senhas contra listas de vazamentos em vez de impor regras de composição, oferecer autenticação multifator resistente a phishing, regenerar o identificador de sessão no login, expirar e invalidar sessões no logout e na troca de senha, tornar os tokens de redefinição de uso único e de curta duração, aplicar atrasos progressivos em vez de bloqueios permanentes e manter as mensagens idênticas, exista a conta ou não. O panorama completo está em gestão de identidades e acessos.

Falhas de integridade de software e de dados

Esta categoria cobre confiar em código ou dados cuja procedência você não verificou: um build que baixa um plugin de uma fonte sem versão fixada, um pipeline com acesso de escrita à produção e sem revisão da sua própria configuração, um mecanismo de atualização automática sem verificação de assinatura e — o caso clássico no nível da aplicação — desserializar dados controlados por um atacante em tipos arbitrários.

// Vulnerável: a desserialização nativa de bytes não confiáveis pode instanciar qualquer coisa que o classpath permita
Object payload = new ObjectInputStream(request.getInputStream()).readObject();

// Corrigido: um formato de dados sem resolução de tipos, vinculado a uma estrutura conhecida
OrderRequest payload = jsonMapper.readValue(request.getInputStream(), OrderRequest.class);

Além disso, fixe as versões das dependências com checksums, assine os artefatos e verifique as assinaturas antes do deploy, submeta os sistemas de build ao mesmo controle de acesso da produção e desative o tratamento polimórfico de tipos nas bibliotecas JSON, a menos que uma necessidade específica o justifique e uma allow-list o restrinja.

Falhas de registro e monitoramento de segurança

Não é uma porta de entrada, mas é o motivo de uma invasão durar meses em vez de horas. Os sintomas são reconhecíveis: tentativas de login malsucedidas que não são registradas, nenhum alerta para negações de autorização ou volumes de dados incomuns, logs guardados apenas na máquina que os gerou e nenhuma sincronização de relógio para correlacionar eventos entre serviços.

Registre tentativas de autenticação, negações de controle de acesso, mudanças de privilégio, ações administrativas e falhas de validação de entrada na fronteira, com ator, ação, alvo, resultado, timestamp e identificador de correlação. Envie os logs para fora do host, para um armazenamento que a aplicação não consiga reescrever, mantenha-os por tempo suficiente para investigar e gere alertas sobre padrões, não sobre eventos isolados. Depois, pratique: um alerta a que ninguém nunca respondeu não é um controle. E tome cuidado com o que você grava — senhas, tokens e dados pessoais nos logs criam uma segunda cópia desses dados, com proteção mais fraca.

Server-side request forgery

SSRF acontece quando uma aplicação busca uma URL fornecida por um usuário — o destino de um webhook, um importador de imagens, um conversor de documentos — e o atacante a aponta para dentro, para serviços internos ou para um endpoint de metadados da nuvem que entrega credenciais a qualquer um que as peça a partir da instância.

Mitigue em camadas, porque a validação de URL sozinha é notoriamente furada: valide contra uma allow-list de hosts permitidos em vez de uma deny-list, resolva o hostname e confira se o endereço resultante é público antes de conectar, desative o seguimento de redirects ou revalide cada salto, exija o esquema esperado e dê ao componente que faz as requisições regras de saída de rede que permitam só o que ele legitimamente precisa. Em instâncias na nuvem, exija o serviço de metadados reforçado que requer um token de sessão. Retorne um resultado genérico para quem chamou, para que o conteúdo e o tempo de resposta não virem um scanner de portas interno.

Usando bem a lista

Três hábitos separam os times que tiram proveito do Top 10 dos que apenas o citam.

Trate-o como ponto de partida, não como linha de chegada. É um ranking de categorias comuns, não uma especificação do que é suficiente. A ausência dessas dez fraquezas não significa que uma aplicação seja segura; sua lógica de negócio tem riscos que nenhuma lista genérica consegue nomear.

Transforme conscientização em requisitos. O Application Security Verification Standard é o documento complementar para isso: várias centenas de requisitos testáveis organizados por tema, com três níveis de rigor para que cada time escolha o que corresponde ao seu risco. Escolha um nível, adote os requisitos relevantes como critérios de aceitação e você terá substituído “consideramos a OWASP” por afirmações que um revisor consegue verificar. O API Security Top 10 faz o mesmo papel para os riscos específicos de APIs.

Automatize o que pode ser automatizado e revise o que não pode. Scans de dependências e de configuração pertencem ao pipeline, ao lado da análise estática; falhas de controle de acesso e de lógica de negócio exigem pessoas que entendam o domínio. Acima de tudo, escreva um teste de regressão para cada defeito de segurança corrigido, para que a mesma fraqueza não volte junto com uma nova funcionalidade.

Principais conclusões

  • O Top 10 é um ranking de conscientização de categorias de fraquezas, não um padrão — use o ASVS quando precisar de requisitos testáveis.
  • A quebra de controle de acesso lidera a lista porque depende do domínio e é invisível para scanners; negue por padrão, restrinja as consultas ao principal e teste com um segundo usuário.
  • Injeção se resolve mantendo os dados fora da gramática do interpretador: instruções parametrizadas, arrays de argumentos e codificação de saída contextual.
  • Não há código que contorne um design inseguro; modelagem de ameaças e casos de abuso são a única correção.
  • Configuração incorreta e componentes desatualizados são as categorias em que automatizar rende mais, com infraestrutura como código, SBOMs e scan de dependências a cada commit.
  • Registro, monitoramento e uma resposta ensaiada decidem quanto tempo uma invasão dura, e SSRF exige defesa em camadas tanto no nível da URL quanto no da rede.

Fontes e leituras complementares

Perguntas frequentes

O OWASP Top 10 é um padrão de segurança?
Não, e a própria OWASP diz isso explicitamente. É um documento de conscientização, construído a partir de dados sobre quais categorias de fraquezas aparecem com mais frequência, pensado para iniciar conversas e definir prioridades. Um time que corrigiu as dez ainda tem trabalho pela frente. Quando você precisa de requisitos testáveis — algo com que um revisor possa aprovar ou reprovar um build —, o documento escrito para isso é o Application Security Verification Standard.
Por que a quebra de controle de acesso fica sempre em primeiro lugar?
Porque ela não pode ser encontrada por busca de padrões. Um scanner automatizado vê uma requisição que retorna dados e não tem como saber se aquele usuário tinha direito a eles; só alguém que entende o domínio pode dizer. Além disso, o controle de acesso está espalhado por todos os endpoints, então basta uma rota nova adicionada sem a verificação para reabrir o risco, por melhor que seja o resto da aplicação.
Como manter as dependências vulneráveis sob controle?
Gere uma lista de materiais de software (SBOM) no momento do build, rode um scanner de dependências a cada commit contra uma base de alertas de segurança mantida e automatize as atualizações de rotina para que só as difíceis precisem de atenção humana. Fixe versões e verifique checksums para que o build seja reproduzível, remova as bibliotecas que você não usa mais e acompanhe o grafo de dependências transitivas, não só as declarações diretas.
Qual é a diferença entre design inseguro e um bug de segurança?
Um bug é um design correto implementado de forma errada — uma verificação ausente, uma comparação incorreta — e se corrige no código. Design inseguro é um design fiel à sua especificação que não tem como ser tornado seguro, como uma redefinição de senha baseada em perguntas cuja resposta qualquer um consegue pesquisar. Nenhum cuidado na codificação resolve isso; é preciso mudar o mecanismo, e é por isso que a modelagem de ameaças vem antes da implementação.