Pular para o conteúdo
Quiz
← Início

Segurança

100 questões

Certas
0
Erradas
0
Não respondidas
100
Dificuldade
Número de questões

100 questões neste filtro

Resumo teórico

Imagine a sua casa. Ela tem porta da frente, janelas, uma fechadura, uma chave debaixo do tapete e um vizinho que às vezes pede açúcar emprestado. Segurança é o trabalho de pensar em tudo isso: quem pode entrar, o que pode levar e o que fazemos para que a pessoa errada não consiga. Software também tem portas e chaves. Um site tem um login (a porta da frente), cookies (bilhetinhos que dizem "essa pessoa já entrou") e um banco de dados (o armário onde fica tudo que vale a pena guardar).

A maioria das invasões na internet não é coisa de filme, com hacker digitando rápido. Elas acontecem porque um programador confiou em algo que deveria ter verificado: um nome digitado numa caixinha, um número no endereço, um arquivo que alguém enviou. Esta matéria, chamada segurança de aplicações, ensina como os programas falham quando alguém os usa de um jeito que ninguém esperava, para que a gente os construa de modo que não falhem.

Tudo aqui é defensivo. Para cada falha explicamos por que ela acontece e, sempre, como evitá-la. O código que você vê é o menor exemplo necessário para reconhecer o problema, e vem sempre ao lado da correção. As ideias seguem os guias do , a referência padrão da área.

No final você vai conhecer as principais famílias de falhas da web (injeção, scripts plantados em páginas, requisições forjadas, senhas fracas, sessões roubadas, tokens quebrados, falta de checagem de permissão, servidores enganados, uploads inseguros), os hábitos que barram cada uma e o básico de criptografia e segredos que sustenta todo o resto.

Como a web funciona: as peças com que um atacante brinca

Antes de estudar como as coisas quebram, precisamos saber como um site normalmente funciona. Pense num restaurante. Você (o navegador) senta à mesa e escreve um pedido num papel. O garçom leva até a cozinha (o servidor). A cozinha cozinha e o garçom traz o prato. O papel do pedido se chama , e o prato é a resposta.

  Navegador                          Servidor
     |                                  |
     |  GET /invoices/1001  + cookie    |
     | -------------------------------> |  busca os dados,
     |                                  |  monta uma página
     |  200 OK  + página (HTML)         |
     | <------------------------------- |
     |                                  |
Uma ida e volta entre navegador e servidor

Algumas palavras vão voltar o tempo todo. Um cookie é um bilhetinho que o servidor pede ao navegador para guardar e mostrar de novo em toda visita seguinte, como a pulseira de um parque aquático: os funcionários não lembram do seu rosto, só conferem a pulseira. Uma origem é o endereço exato de um site: o protocolo (https), o nome (shop.test) e a porta. Duas páginas de origens diferentes são estranhas uma para a outra, e o navegador faz valer isso com a .

PalavraImagem do dia a diaO que é
ClienteO freguês na mesaO navegador ou app do usuário. O usuário o controla por completo
ServidorA cozinhaO programa que escrevemos e protegemos
CookieUma pulseiraUm bilhete que o navegador devolve automaticamente a cada requisição
OrigemUm endereço de rua completoProtocolo + nome + porta, por exemplo https://shop.test:443
Entrada não confiávelO bilhete de um estranhoTudo que vem de fora: formulários, URLs, cabeçalhos, arquivos e até dados lidos de volta do banco
↑ Voltar ao sumário

OWASP Top 10, ameaças e bons hábitos

Por onde começar? Médicos têm uma lista das doenças mais comuns. Quem trabalha com segurança também. O OWASP Top 10 é um documento de conscientização, não é lei nem certificado. Ele agrupa falhas em dez categorias de risco, ordenadas pela frequência com que aparecem em dados reais e por uma pesquisa com profissionais. É um bom ponto de partida para estudar e revisar, mas não é uma lista completa de todas as falhas, e passar numa revisão baseada nele não prova que uma aplicação é segura.

Neste resumo usamos a edição de 2021. A categoria número um é Broken Access Control (controle de acesso quebrado): uma pessoa logada alcança coisas que não são dela. A injeção, que liderava a edição de 2017, foi para o terceiro lugar.

OWASP Top 10, edição 2021
PosiçãoCategoriaEm uma frase
A01Broken Access ControlUm usuário lê ou altera o que não deveria
A02Cryptographic FailuresDados sensíveis não são protegidos, ou são protegidos mal
A03InjectionDados se misturam a código (SQL, shell) e rodam como código
A04Insecure DesignO próprio plano não tem um controle necessário
A05Security MisconfigurationPadrões, recursos abertos e erros barulhentos esquecidos em produção
A06Vulnerable and Outdated ComponentsBibliotecas antigas com falhas conhecidas
A07Identification and Authentication FailuresSenhas fracas, sessões fracas, login fraco
A08Software and Data Integrity FailuresAtualizações e dados aceitos sem checar de onde vieram
A09Security Logging and Monitoring FailuresNinguém percebe a invasão
A10Server-Side Request Forgery (SSRF)O servidor é enganado e chama lugares que não deveria

Pensando como defensor: modelagem de ameaças

é responder quatro perguntas cedo: O que estamos construindo? O que pode dar errado? O que vamos fazer a respeito? Fizemos um bom trabalho? Ela rende mais na fase de projeto, antes de existir código, porque mudar um desenho é barato e mudar um sistema pronto é caro. Quando a arquitetura muda, você faz de novo.

Uma lista prática para "o que pode dar errado" é o STRIDE. Cada letra é um tipo de ameaça, e cada uma quebra uma propriedade que queremos manter. Saber qual propriedade foi quebrada indica o tipo de controle a usar.

STRIDE
LetraAmeaçaPropriedade quebradaControle típico
SSpoofing: fingir ser outra pessoaAutenticidadeAutenticação forte
TTampering: alterar dadosIntegridadeAssinaturas, checagens no servidor
RRepudiation: negar que fez algoNão repúdioLogs e trilhas de auditoria
IInformation disclosure: vazar segredosConfidencialidadeCriptografia, controle de acesso
DDenial of service: fazer parar de funcionarDisponibilidadeLimites, capacidade, filtragem
EElevation of privilege: ganhar mais direitosAutorizaçãoChecagem de permissões, privilégio mínimo

Princípios que aparecem em todo lugar

  • Privilégio mínimo: cada pessoa, programa ou conta de banco recebe só as permissões de que o trabalho precisa. Quem cuida do bichinho da turma precisa da chave da sala, não da escola inteira. Se o app só lê produtos, a conta dele no banco não pode ter permissão de apagar tabelas.
  • Defesa em profundidade: empilhe várias proteções independentes, porque qualquer uma pode falhar. Um castelo tem fosso, muralha e guardas.
  • Seguro por padrão e negar por padrão: a escolha segura é a que você recebe quando não faz nada.
  • Design inseguro versus implementação insegura: um bug é código que não faz o que o plano dizia. Design inseguro é código que faz exatamente o que o plano dizia, quando o plano esqueceu um controle. Exemplo: uma loja deixa qualquer pessoa reservar estoque ilimitado por 24 horas sem pagar. Robôs reservam tudo. O código não tem bug; falta um limite na regra. A correção é redesenhar a regra, e a prevenção é escrever casos de abuso ao lado dos casos de uso.
  • Configuração insegura: o código está bom, mas a implantação não. Contas padrão admin/admin, listagem de diretórios ligada e páginas de erro mostrando o stack trace completo são exemplos clássicos. A cura é um processo de endurecimento (hardening) repetível, igual em todos os ambientes.
↑ Voltar ao sumário

Criptografia e TLS: códigos secretos que protegem a estrada

é a caixa de ferramentas por trás das senhas, dos tokens e do cadeado do navegador. Você não precisa da matemática. Precisa saber qual ferramenta responde a qual pergunta.

Duas maneiras de trancar uma mensagem

A criptografia simétrica usa a mesma chave para trancar e destrancar, como um diário com uma chave que os dois amigos têm. É muito rápida, mas há um problema: como os dois combinam a chave sem que ninguém mais escute? A criptografia assimétrica usa um par de chaves. A chave pública pode ser mostrada ao mundo todo, como a abertura da caixa de correio onde qualquer um joga cartas. A chave privada fica em segredo, como a chave que abre a caixa. Ela resolve o problema de compartilhar, mas é bem mais lenta.

AspectoSimétricaAssimétrica
ChavesUma chave secreta compartilhadaUma chave pública e uma chave privada
VelocidadeMuito rápidaBem mais lenta
Problema principalComo compartilhar a chave com segurançaO custo, por isso é usada só por um instante
No TLS serve paraCifrar todos os dados da conexãoO handshake: provar quem é o servidor e combinar as chaves

Sistemas reais usam as duas, o que se chama criptografia híbrida. O TLS usa a ferramenta assimétrica, lenta, uma vez no início, para combinar um segredo sem nenhum segredo anterior. Depois disso, tudo é protegido com a ferramenta simétrica, rápida.

Hash não é criptografia

Uma função hash é como um moedor de carne: qualquer entrada entra, uma "impressão digital" de tamanho fixo sai, e não dá para transformar a carne de volta. Ela é de mão única. Uma cifra é uma tranca reversível: com a chave você recupera o original. Elas atendem a necessidades diferentes, e usar uma no lugar da outra é um erro clássico.

FerramentaReversível?Tamanho da saídaUse para
Hash (por exemplo SHA-256)NãoSempre igualVerificar que um arquivo não mudou
Cifra (por exemplo AES)Sim, com a chaveCerca do tamanho da entradaManter dados em segredo
MAC / HMACNãoFixoVerificar que uma mensagem não foi alterada por quem não tem a chave
Assinatura digitalNãoFixoProvar quem escreveu uma mensagem quando ninguém compartilha chave

Por que um hash simples não basta para proteger uma mensagem no caminho? Porque se alguém no meio alterar a mensagem, basta calcular um novo hash para a nova mensagem. O hash não tem segredo nenhum. Um como o HMAC mistura uma chave secreta ao hash. Sem a chave, ninguém produz um valor válido para uma mensagem alterada. Quando os dois lados não compartilham chave, uma assinatura digital faz o mesmo papel.

TLS: o que o cadeado realmente significa

O TLS é o protocolo por trás do https. Trancar a conversa não basta, porque você pode estar trancando para a pessoa errada. Por isso o servidor mostra um certificado, um documento assinado por uma autoridade confiável dizendo "esta chave pública pertence a este domínio". O navegador o valida. O que isso prova? Que a chave pública realmente pertence ao domínio que você visita, e que o servidor tem a chave privada correspondente. Não prova que o site é honesto. Um site golpista pode ter um cadeado perfeitamente válido.

1. Navegador: "oi, quero shop.test"
2. Servidor  : certificado (chave pública + assinatura)
              navegador confere: é de shop.test?
3. Ambos     : combinam chaves de sessão novas
              (assimétrica, Diffie-Hellman efêmero)
4. Daí em diante: criptografia simétrica rápida
              + checagem de integridade em cada mensagem
TLS em passos

Há mais uma propriedade legal. Suponha que alguém grave todo o seu tráfego cifrado hoje e, daqui a cinco anos, roube a chave privada do servidor. Dá para ler as gravações antigas? Com o TLS 1.3, não. Isso se chama sigilo perfeito de encaminhamento (forward secrecy). Cada conexão cria suas chaves com uma troca Diffie-Hellman , com números aleatórios descartados no fim. A chave do certificado serve só para provar identidade, não para proteger o tráfego, então roubá-la depois não abre nada do passado.

↑ Voltar ao sumário

Injeção: quando dados viram comandos

Imagine que você diz a um garçom robô: "Traga a sopa". Agora uma criança travessa escreve na comanda: "sopa, e também abra o caixa". Se o robô lê a comanda como uma lista longa de instruções, ele obedece às duas. Isso é injeção: o programa deveria tratar o que o usuário escreveu como simples dados, mas deixa esses dados serem lidos como código. Os dois se misturaram num mesmo pedaço de texto.

Na injeção de SQL o "robô" é o banco de dados. O programa monta a consulta colando textos, então um valor com uma aspa pode mudar a forma do comando. O problema é a cola, não o usuário.

const email = body.email; // vem do usuário
const sql = "SELECT id, name FROM users WHERE email = '" + email + "'";
const result = await db.query(sql);
// um email como  x' OR '1'='1  muda o significado da consulta
Vulnerável: o valor vira parte do texto SQL
const sql = "SELECT id, name FROM users WHERE email = $1";
const result = await db.query(sql, [email]);
// o banco fixa a forma do comando primeiro,
// e só depois recebe email como valor
Corrigido: consulta parametrizada mantém texto e valores separados

Por que a segunda versão funciona? O banco primeiro lê o texto com os marcadores ($1, ou ? em outros bancos) e decide o que é o comando. Só depois recebe os valores, e um valor só pode preencher uma posição de dado. Então x' OR '1'='1 é só um email estranho para comparar, não uma condição. Isso se chama , e é a defesa principal porque remove a causa.

Outras defesas e coisas que não funcionam

  • Validação (o valor tem o tamanho, o tipo e o formato certos?) é uma boa camada extra. Ela decide se um valor faz sentido. Não substitui parâmetros. Um campo de sobrenome ainda deve aceitar O'Brien, então você aceita o apóstrofo e envia o valor como parâmetro.
  • Privilégio mínimo: uma conta de banco que só lê o que precisa limita o estrago se algo escapar.
  • Listas de bloqueio e escape manual (remover aspas, banir palavras) tratam sintomas e são fáceis de contornar. Não conte com elas.
  • ORMs (bibliotecas que escrevem o SQL por você) parametrizam seus métodos normais, então reduzem muito o risco. Mas SQL cru montado com interpolação de strings dentro de um ORM é tão injetável quanto qualquer outro.
  • Dados do seu próprio banco também não são confiáveis. Se um nome foi guardado com segurança e depois colado em outra consulta, essa segunda consulta é vulnerável. Parametrizar é regra para toda consulta, não só para o que acabou de chegar.
  • Stored procedures não são mágica. Se uma procedure monta SQL dinâmico colando o argumento numa string, ela é injetável. Dentro do banco, o SQL dinâmico precisa dos próprios parâmetros (no PostgreSQL, EXECUTE ... USING).

Quando a estrutura precisa mudar

Marcadores representam valores, não nomes de colunas, nomes de tabelas ou ASC/DESC. Suponha que uma lista possa ser ordenada por price ou name. Não cole a palavra recebida na consulta. Use a palavra só como chave de uma lista fixa e pegue o nome real da coluna dessa lista.

const columns: Record<string, string> = { price: "price", name: "name" };
const column = columns[query.sort];
if (!column) return status(400);
const sql = `SELECT id, name FROM products ORDER BY ${column}`;
// o texto vem da nossa lista, nunca do usuário
Lista de permissão para uma parte estrutural

Parâmetros também não mudam o que um valor significa dentro de uma mini-linguagem própria. Em LIKE '%' || $1 || '%', se o usuário enviar %, não há injeção, mas % ainda funciona como o curinga "qualquer coisa" e casa com todas as linhas. Se a busca precisa ser literal, escape % e _ no valor.

A mesma ideia com o sistema operacional

A injeção de comandos é o mesmo erro com outro interpretador: o shell. Um servidor que monta o texto ping -c 1 mais um host digitado pelo usuário e entrega a um shell está em apuros, porque caracteres como ;, | e && significam "rode outro comando" para um shell. A cura: não use o shell. Inicie o programa diretamente, informando o executável e uma lista de argumentos, para que cada argumento chegue como dado, e valide o host contra um formato estrito.

// Vulnerável: um shell lê a string inteira
exec("ping -c 1 " + host);

// Corrigido: sem shell, argumentos em lista, host validado antes
if (!/^[a-zA-Z0-9.-]{1,253}$/.test(host)) return status(400);
spawn("ping", ["-c", "1", host]);
Vulnerável e corrigido
↑ Voltar ao sumário

Cross-site scripting (XSS): o script de um estranho na sua página

A injeção tem uma prima em que o "robô" é o navegador. Um navegador lê texto HTML e constrói a página a partir dele. Alguns pedaços de HTML são código, como um bloco <script> ou um atributo como onerror="...". No cross-site scripting (XSS), um script escrito por outra pessoa vai parar dentro da sua página, e o navegador não consegue saber que não foi você quem o escreveu. Ele roda com a origem do site vulnerável, então enxerga a página, lê o que a página pode ler e age em nome de quem está com a página aberta.

A causa raiz é sempre a mesma: dados foram escritos num lugar que é interpretado como código sem serem codificados para aquele lugar. Há três tipos clássicos, e a diferença é só como os dados chegam à página.

TipoComo os dados chegamExemplo
Armazenado (stored)O servidor guarda e serve a todos que abrem a páginaUm comentário com uma tag de script, mostrado a todo leitor do artigo
Refletido (reflected)O servidor devolve na hora algo que veio na requisiçãoUma página de busca que imprime o texto buscado sem alterar
Baseado em DOMO JavaScript da própria página lê os dados e os insere na página. O servidor pode nunca vê-losUm script que coloca na página a parte depois do # do endereço

Defesa 1: codificar na saída

Ao escrever dados do usuário em HTML, transforme os caracteres de significado especial em equivalentes inofensivos: < vira &lt;, > vira &gt;, & vira &amp;, e as aspas também são trocadas. O navegador então mostra os caracteres como texto em vez de lê-los como tags. Nada é apagado; o visitante vê exatamente o que foi digitado.

Sem codificação:
  servidor escreve : <span> <script>bad()</script> </span>
  navegador vê     : uma tag script -> EXECUTA

Com codificação:
  servidor escreve : <span> &lt;script&gt;bad()&lt;/script&gt; </span>
  navegador vê     : texto puro     -> mostrado, nunca executa
Por que a codificação barra o script

Codifique na hora da saída, não ao salvar no banco. A codificação certa depende de onde o dado cai (texto HTML, atributo, URL, JavaScript, CSS), e isso você só sabe no momento de escrever a página. Um dado guardado já codificado para HTML ficaria errado numa API JSON e continuaria inseguro dentro de JavaScript. O contexto importa: algo inofensivo num lugar pode ser código em outro. Num href ou src, por exemplo, o esquema da URL também precisa ser checado, porque um link javascript: executa código ao ser clicado. Aceite só http: e https:, de uma lista de permissão.

Defesa 2: APIs seguras do navegador

No XSS baseado em DOM o servidor não tem nada para codificar, porque a parte perigosa nunca viaja até ele. A correção fica dentro do script: use APIs que tratam o valor como texto.

const name = decodeURIComponent(location.hash.slice(1));

// Vulnerável: innerHTML interpreta o texto como HTML
// document.querySelector('#greeting').innerHTML = 'Olá, ' + name;

// Corrigido: textContent sempre insere texto puro
document.querySelector('#greeting').textContent = 'Olá, ' + name;
Um XSS baseado em DOM e sua correção
Seguro: trata o valor como textoPerigoso: interpreta o valor como marcação
textContent, createTextNodeinnerHTML, outerHTML
setAttribute para atributos que não executam códigodocument.write, insertAdjacentHTML

Frameworks, sanitizadores e o que sobra

  • Frameworks modernos de interface codificam automaticamente os valores que você interpola nos templates. O XSS volta quando você usa a saída de "HTML cru" com dados do usuário, coloca uma URL do usuário num href ou mexe direto no DOM.
  • Se os usuários precisam poder enviar formatação (negrito, listas, links), a codificação a estragaria. Passe o HTML por um sanitizador mantido que preserve só uma lista de permissão de tags, atributos e esquemas de URL. Listas de permissão falham de forma segura: tudo que é desconhecido é removido. Não escreva um sanitizador você mesmo; são muito difíceis de acertar.
  • Cookies HttpOnly são uma mitigação, não uma correção. O script não consegue ler aquele cookie, mas ainda pode enviar requisições como a vítima, porque o navegador anexa o cookie sozinho. A correção continua sendo codificação e destinos seguros, com a Content Security Policy como camada extra (próxima seção).
↑ Voltar ao sumário

Content Security Policy e cabeçalhos de segurança

A codificação é a defesa principal contra o XSS, mas programadores esquecem coisas. Então acrescentamos uma rede de segurança: dizemos ao navegador, de antemão, o que a página pode fazer. Pense numa excursão da escola: a professora entrega ao motorista do ônibus uma lista escrita dos lugares que a turma pode visitar. Se um estranho tentar incluir uma parada, o motorista recusa. Essa lista é uma Content Security Policy (CSP).

A CSP é um cabeçalho HTTP enviado pelo servidor. É uma lista de permissão aplicada pelo navegador: diz de quais origens a página pode carregar e executar conteúdo. Ela reduz o estrago quando a codificação de saída falha em algum ponto, mas não a substitui.

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'
Uma política estrita
DiretivaSignificado
script-src 'self'Só arquivos de script da mesma origem rodam. Blocos <script> inline e handlers inline como onclick são recusados
default-src 'self'Tudo que não foi listado (imagens, estilos, conexões) só da mesma origem
object-src 'none'Sem plugins como <object> ou <embed>
base-uri 'none'Sem elemento <base> que poderia mudar para onde apontam os endereços relativos
form-action 'self'Formulários só podem ser enviados para a mesma origem
frame-ancestors 'none'Ninguém pode colocar esta página dentro de um <iframe>

Com script-src 'self', se um comentário mal codificado deixar passar um script inline para a página, o navegador roda /app.js (um arquivo da mesma origem) e bloqueia o script inline. Esse é o ganho principal: a maioria das injeções parece marcação dentro da página. Para se beneficiar, o seu JavaScript precisa ficar em arquivos, não em atributos de evento.

Não quebre a rede

Quando código antigo precisa de scripts inline, alguém pode ser tentado a escrever script-src 'self' 'unsafe-inline'. A palavra-chave tem esse nome por um motivo: ela devolve ao atacante o caminho mais fácil, e a política passa a valer pouco contra XSS. As formas certas de manter um script inline necessário são movê-lo para um arquivo ou autorizá-lo individualmente com um nonce ou um hash.

Um é colocado tanto no cabeçalho (script-src 'nonce-R4nd0m') quanto no atributo nonce das suas próprias tags de script. Um script injetado não conhece o valor desta resposta e é bloqueado. Se o valor for fixo num arquivo de configuração, ele é igual em toda página, qualquer um pode lê-lo uma vez e colocá-lo na própria tag, e a proteção acaba.

Implantando com segurança

Num site grande e antigo, ligar uma política estrita pode quebrar páginas que usam algo que você esqueceu. O caminho seguro é o Content-Security-Policy-Report-Only. O navegador avalia a política sem aplicá-la e envia relatórios de violação. Corrija o que os relatórios mostrarem e, quando estiverem limpos, mova a mesma política para o cabeçalho Content-Security-Policy de verdade.

Outros cabeçalhos de segurança

CabeçalhoO que fazPor que importa
Strict-Transport-Security (HSTS)Diz ao navegador para usar só HTTPS neste domínio durante o max-ageSem ele, a primeira requisição pode ir por HTTP e alguém no caminho pode impedir a troca para HTTPS. Só vale quando recebido por HTTPS
X-Content-Type-Options: nosniffO navegador deve respeitar o Content-Type declarado e não adivinharImpede que um arquivo enviado cheio de HTML seja executado porque o navegador achou que era uma página
frame-ancestors (na CSP)Declara quem pode colocar a página num frameDefende contra clickjacking: sua página carregada invisível no frame de outro site para roubar cliques. 'none' não permite ninguém, 'self' só a sua própria origem
↑ Voltar ao sumário

CSRF e cookies SameSite: requisições forjadas

Imagine uma pizzaria que entrega para quem tiver o seu cartão fidelidade. Um brincalhão não precisa roubar o cartão. Ele só grita "Ei, pizzaria, entregue 50 pizzas neste endereço!" de um jeito que faz o seu próprio celular fazer o pedido, e a pizzaria vê o número do seu cartão e entrega. Isso é cross-site request forgery (CSRF): outro site faz o navegador da vítima enviar uma requisição que o site alvo aceita como se fosse da vítima.

Repare no que o atacante não faz. Ele não lê a resposta nem rouba a senha. Só precisa que uma ação aconteça com os direitos da vítima, como trocar um e-mail ou transferir dinheiro. Por isso o CSRF mira requisições que mudam estado.

Ele funciona por causa de como os cookies se comportam. Um cookie de sessão é uma credencial ambiente: o navegador o anexa sozinho a toda requisição para aquele site, mesmo quando a requisição foi iniciada por uma página de outro site. O servidor recebe uma requisição perfeitamente válida e não tem como saber que o usuário não quis aquilo.

  página evil.test          Navegador            bank.test
      |  form oculto p/ bank.test  |                    |
      | -------------------------> |                    |
      |                            | POST /transfer     |
      |                            | + cookie bank.test |
      |                            | -----------------> |
      |                            |  servidor: sessão  |
      |                            |  válida, executa!  |
Como uma requisição forjada viaja

Defesa 1: cookies SameSite

O atributo SameSite diz ao navegador quando o cookie pode ser enviado em requisições entre sites. Um site é o domínio registrável (por exemplo example.com), uma ideia maior que a de origem.

ValorO cookie é enviadoContrapartida
StrictSó em requisições feitas a partir do mesmo siteO mais seguro. Quem chega por um link externo parece "deslogado" na primeira página
LaxTambém em navegações de nível superior com método seguro, como clicar num link (GET)Um bom padrão, mas supõe que GET não tem efeitos colaterais
NoneSempre, inclusive entre sites; exige SecureNenhuma proteção contra CSRF

O Lax não é mágico. Se o seu site tem GET /account/delete, um link em outro site apontando para lá será clicado pela vítima e o navegador envia o cookie, então a conta é apagada. Nunca altere dados em GET. Use POST, PUT, PATCH ou DELETE, protegidos por um token.

Lembre também que o SameSite protege contra outros sites, não contra outras origens do mesmo site. Se app.example.com e pages.example.com compartilham o domínio registrável example.com, e clientes publicam o próprio HTML em pages, então uma página publicada lá é do mesmo site que a sua aplicação. O SameSite=Strict não segura o cookie. Coloque conteúdo não confiável num domínio registrável separado e mantenha o token como defesa principal.

Defesa 2: o token anti-CSRF

No padrão do token sincronizador, o servidor gera um valor aleatório ligado à sessão, coloca-o num campo oculto dos seus formulários e confere em todo POST. O cookie vai sozinho, mas o token não é enviado automaticamente. A página forjada não consegue ler a sua página real para copiar o token, porque a proíbe.

<form method="post" action="/transfer">
  <input type="hidden" name="csrf" value="9f3a...aleatorio-por-sessao">
  <input name="amount">
</form>
<!-- servidor: recusa o POST se csrf faltar ou não bater -->
Um formulário com token

Isso também explica por que o XSS derrota o token: um script injetado está na mesma origem, então tem permissão para ler o token.

Dois mal-entendidos comuns

  • "Não configuramos CORS, então outros sites estão bloqueados." Errado. A política de mesma origem e o CORS protegem a leitura de respostas entre origens. Eles não impedem que um POST simples de formulário seja enviado e processado. O CSRF não precisa ler nada. A defesa precisa viver no servidor, com token e SameSite.
  • "Nossa API usa um token Bearer, estamos expostos?" Pouco. Se o token fica na memória e é enviado explicitamente no cabeçalho Authorization, o navegador não o anexa a requisições forjadas vindas de outro site. O CSRF é um problema de credenciais ambiente: cookies, autenticação Basic, certificados de cliente. O preço é que scripts da página alcançam o token, então a defesa contra XSS pesa ainda mais.
↑ Voltar ao sumário

Autenticação: senhas, hashing e mais de um fator

Duas palavras são fáceis de confundir. Autenticação responde "quem é você?". Autorização responde "você pode fazer isso?". Num cinema, mostrar o ingresso na porta é autenticação; poder entrar só na sala 3 e não na sala 5 é autorização. Um sistema pode fazer a primeira com perfeição e a segunda mal, e vice-versa. São controles diferentes, com falhas diferentes.

Como guardar uma senha

O servidor nunca precisa saber a sua senha, só reconhecê-la. Então guarda a saída de uma função de mão única e, no login, repete o cálculo com o que você digitou e compara. Senhas não são criptografadas (quem pegasse a chave levaria todas as senhas); elas passam por hash.

Dois ingredientes tornam um hash adequado para senhas. Primeiro, um salt: um valor aleatório, diferente para cada senha e guardado junto do hash. Sem ele, dois usuários com a mesma senha têm o mesmo hash, e um atacante pode usar uma tabela pré-calculada de senhas comuns para quebrar muitas contas de uma vez. Com salt, cada linha é um problema separado. O salt não é secreto. Segundo, o hash precisa ser lento de propósito.

Por que lento? Hashes rápidos como MD5 ou SHA-256 foram feitos para conferir arquivos depressa, o que é ótimo para arquivos e péssimo para senhas, porque quem tem uma tabela roubada pode tentar milhões de palpites por segundo. Argon2id, scrypt e bcrypt adicionam um custo que você pode aumentar com o tempo; Argon2id e scrypt também exigem muita memória, o que torna as placas de vídeo (GPUs) caras de usar. Um SHA-256 com salt continua inadequado: o salt impede o ataque em massa, mas só o custo atrasa cada conta individual.

Palpites por segundo em um núcleo de CPU, do benchmark deste repositório (lab de senhas e sessões)
MD5, sem salt
843,636 hashes por segundo
SHA-256 + salt de 16 bytes
548,724 hashes por segundo
Argon2id 19 MiB, t=2
22 hashes por segundo
Argon2id 64 MiB, t=3
3.5 hashes por segundo

Leia o gráfico como o número de palpites por segundo que um atacante pode tentar numa tabela roubada, por núcleo. O Argon2id faz cada palpite custar cerca de 45 milissegundos e dezenas de megabytes de memória, aproximadamente 38.000 vezes mais que o MD5 naquela execução. Um usuário paga esse custo uma vez por login e não percebe. Máquina, versões e comando estão registrados nos resultados do lab.

EsquemaMão únicaSaltLento e exigente em memória
Texto puronãonãonão
MD5simnãonão
SHA-256 com saltsimsimnão
Argon2idsimsimsim

Um pepper é diferente de um salt. O pepper é um segredo compartilhado por toda a aplicação e guardado fora do banco de dados (num cofre ou no ambiente). Ele ajuda quando só o banco vaza, por exemplo por uma injeção de SQL ou um backup roubado. Se o servidor inteiro for tomado, o pepper vaza junto, então é uma camada extra, não uma substituição.

E se um sistema antigo guarda só MD5(senha) e queremos todos protegidos por Argon2id agora, sem saber as senhas? Guarde Argon2id(MD5 antigo) para todas as contas de uma vez e registre qual esquema cada linha usa. No próximo login bem-sucedido de cada usuário, você tem a senha de verdade em mãos, então guarde o Argon2id simples e descarte o invólucro.

Telas de login e políticas

  • Diga a mesma coisa em toda falha. Se a tela responde "usuário não encontrado" para e-mails desconhecidos e "senha errada" para os conhecidos, você entrega uma lista de quem tem conta. Responda "credenciais inválidas" sempre, com o mesmo código de status e tempo parecido. O mesmo vale para as telas de cadastro e de recuperação de senha.
  • Limite tentativas, mas pense em quem controla o contador. Bloquear uma conta para sempre após 3 senhas erradas deixa qualquer um bloquear qualquer um. Use bloqueios curtos que crescem, limites por conta e por origem, e um segundo fator.
  • Siga a orientação atual (NIST SP 800-63B). Exija um tamanho mínimo, aceite frases longas e qualquer caractere, permita colar, recuse senhas já encontradas em vazamentos e não force trocas periódicas sem motivo. Tamanho e unicidade valem mais que regras estranhas de complexidade.

Fatores

Um fator é uma categoria de prova. Há três: algo que você sabe (senha, PIN), algo que você tem (um celular, uma chave de segurança) e algo que você é (impressão digital, rosto). A autenticação multifator de verdade combina categorias diferentes, para que roubar uma senha não baste. Uma senha mais um código temporário de um app autenticador são dois fatores. Uma senha mais uma pergunta secreta não são: ambas são "algo que você sabe".

↑ Voltar ao sumário

Sessões e cookies: continuar logado

Depois de entrar, você não quer digitar a senha a cada página. Então o servidor dá ao seu navegador um identificador de sessão dentro de um cookie, como a pulseira de um parque de diversões: mostre-a e você passa. Isso também significa que a pulseira é uma senha temporária. Quem a apresenta é tratado como você.

Por isso o identificador precisa ser longo, aleatório e sem significado: gerado por um gerador aleatório criptográfico com pelo menos 128 bits de entropia (imprevisibilidade), e apenas uma chave para dados que o servidor guarda. Um identificador adivinhável é uma senha adivinhável. Eis um bom cookie:

Set-Cookie: __Host-sid=<32 bytes aleatórios>; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=28800
Um cookie de sessão bem protegido
AtributoO que fecha
HttpOnlyScripts da página lendo o cookie por document.cookie
SecureO cookie viajando por HTTP puro, onde poderia ser capturado. Até uma única requisição por uma imagem o vazaria. Anda junto com o HSTS
SameSite=LaxO cookie sendo anexado a requisições em segundo plano iniciadas por outros sites
prefixo __Host-Outro subdomínio ou uma página HTTP definindo ou sobrescrevendo o seu cookie. O navegador só aceita o nome se o cookie tiver Secure, Path=/ e nenhum Domain
Max-AgeO navegador guardando o cookie mais tempo do que você quer (o servidor também aplica o limite)

O prefixo __Host- precisa de explicação. Cookies não seguem a mesma ideia de origem do resto da web: um subdomínio pode definir um cookie para o domínio pai e assim sobrescrever o da própria aplicação. O prefixo fecha essa porta e defende contra a fixação vinda de subdomínios menos confiáveis.

Fixação de sessão

Aqui o atacante não rouba a pulseira; ele entrega uma que já conhece e espera você fazer login. Se o servidor mantém o mesmo identificador depois do login, a sua autenticação promove a sessão que o atacante está segurando.

atacante -> navegador da vítima : id de sessão X (plantado)
vítima   -> servidor            : POST /login + cookie X
servidor                        : X agora é "alice"  (mesmo id mantido)
atacante -> servidor            : GET /me + cookie X  => dados da alice

Correção: no login, crie um id NOVO e destrua o antigo.
Fixação de sessão

A regra: troque o identificador a cada mudança de privilégio (login, logout, elevação de acesso).

Encerrar uma sessão e seus limites

  • Logout é uma operação do servidor. Apagar o cookie no navegador não basta. Se o registro continua válido no servidor, quem copiou o identificador antes (de um computador compartilhado, de um log, de um script) ainda pode usá-lo. Apague ou invalide o registro.
  • Nunca coloque o identificador na URL (/orders?sid=...). URLs são copiadas, guardadas no histórico do navegador e em logs de servidores e proxies, e enviadas a outros sites no cabeçalho Referer. Cookies foram feitos para carregá-lo sem esses vazamentos.
  • Use dois tempos limite. O idle timeout (por exemplo 15 minutos sem atividade) reduz a janela de um dispositivo abandonado. O timeout absoluto (por exemplo 8 horas no total) limita a vida de um identificador capturado, porque um id roubado usado continuamente nunca fica ocioso. Aplicações sensíveis usam os dois e renovam o id de tempos em tempos.
↑ Voltar ao sumário

JWT, OAuth 2.0 e OpenID Connect: tokens e login delegado

Alguns sistemas não guardam sessão no servidor. Em vez disso, entregam a você um bilhete assinado, um JSON Web Token (JWT), e confiam no que está escrito nele, desde que a assinatura confira. É como uma pulseira de show com holograma: o segurança não liga para a bilheteria, só confere o holograma. Um JWT são três textos unidos por pontos:

  base64url(header) . base64url(payload) . assinatura
       |                   |                  |
  como foi assinado     as claims         prova de que o emissor
  {"alg":"HS256"}     {"sub":"bob",      escreveu as duas
                        "exp":1800000900}  primeiras partes
Anatomia de um JWT

Dois fatos moldam tudo. Primeiro, o payload é apenas codificado em base64url, não cifrado. Qualquer pessoa com o token o lê com qualquer ferramenta, então nunca coloque segredos ou dados pessoais desnecessários nele. A assinatura garante que nada foi alterado e quem emitiu, não o sigilo. Segundo, o header é escrito por quem envia. Tudo que o verificador lê dele antes de checar a assinatura é uma sugestão de um estranho.

As checagens que um verificador deve fazer

ChecagemPor quê
O alg do header bate com o algoritmo que o servidor esperaSenão quem envia decide se haverá alguma verificação. O algoritmo none significa "sem assinatura"; um verificador que obedece ao token aceita um bilhete forjado
A assinatura é válidaProva que foi emitido por quem tem a chave e não foi alterado
exp é posterior ao momento atual (uma pequena tolerância é aceitável)A expiração limita o estrago de um token vazado. Um verificador que nunca compara exp aceita tokens antigos para sempre
iss é o emissor em quem você confiaO token vem do lugar certo
aud inclui este serviçoUm token feito para o serviço de relatórios não deve funcionar no serviço de pagamentos, mesmo com assinatura correta

Uma assinatura válida sozinha não basta: o token pode ser autêntico mas expirado, ou autêntico mas destinado a outro serviço. Bibliotecas maduras checam tudo isso por padrão e você passa a elas uma lista explícita de algoritmos permitidos (normalmente um). Verificadores escritos à mão costumam esquecer etapas.

Outra armadilha é o segredo. Com HS256, a mesma chave assina e verifica, então toda a força é a aleatoriedade da chave. Se a chave é uma palavra humana como secret, um atacante que tenha um token capturado pode testar palpites no próprio computador, offline, sem tocar no servidor e sem nenhum bloqueio, até um reproduzir a assinatura. Aí ele pode assinar qualquer payload. Uma chave HS256 precisa de pelo menos 256 bits gerados aleatoriamente, nunca uma palavra memorável.

Além disso, um token sem estado não pode ser recolhido: depois de um logout ou roubo ele funciona até o exp. O desenho usual é um token de acesso de vida curta (minutos) mais um refresh token guardado no servidor, rotacionado e revogável.

OAuth 2.0 e OpenID Connect

Imagine que você dá ao manobrista a chave do carro, mas uma chave especial que só abre as portas e liga o motor, não o porta-luvas nem o porta-malas. O OAuth 2.0 é essa ideia: permite que um app aja em seu nome com direitos limitados (os scopes), sem nunca ver a sua senha. Ele resolve a autorização delegada: "este app pode acessar aquele recurso em nome do usuário?" O resultado é um access token destinado a uma API.

O OAuth não diz quem é o usuário. O OpenID Connect (OIDC) é uma camada fina sobre o OAuth que adiciona autenticação. Ele entrega um ID token, um JWT assinado com a identidade do usuário, e um endpoint de informações do usuário.

AspectoAccess tokenID token
RespondeEste app pode chamar aquela API, com quais scopes?Quem fez login?
Destinado aA API (servidor de recursos)O próprio app cliente (o aud é o id do cliente)
Enviado à API?Sim, em Authorization: BearerNão. Uma API que aceita ID tokens deixa de checar se a permissão foi concedida para ela

Apps como aplicativos móveis e single-page applications não conseguem guardar um segredo de cliente. O fluxo recomendado hoje para todo cliente é o fluxo de código de autorização com PKCE, não o antigo fluxo implícito. Com o , o app cria um code_verifier aleatório, envia o hash dele (code_challenge) ao pedir o código e envia o valor original ao trocar o código por tokens. Quem intercepta o código de autorização mas não iniciou o fluxo não tem o verifier, então o código é inútil para essa pessoa.

↑ Voltar ao sumário

Controle de acesso: IDOR, papéis e mass assignment

Numa escola, a carteirinha de estudante deixa você entrar no prédio (autenticação). Mas ela não abre a sala dos professores nem os armários dos outros alunos (autorização). O controle de acesso quebrado acontece quando um programa esquece de checar a segunda parte. Foi a categoria número um do OWASP Top 10 (2021).

A forma mais famosa é o IDOR (insecure direct object reference, referência direta insegura a objeto). Um usuário logado troca /invoices/1001 por /invoices/1002 na barra de endereço e vê a fatura de outra pessoa. O número na URL, no corpo ou num campo oculto é só um pedido. O servidor precisa decidir, a cada requisição, se este usuário pode acessar aquele objeto. Estar logado não basta.

// Vulnerável: sabe quem você é, nunca pergunta se é seu
const invoice = await db.invoices.findById(params.id);

// Corrigido: o dono faz parte da própria consulta
const invoice = await db.invoices.findOne({
	id: params.id,
	ownerId: session.user.id,
});
if (!invoice) return status(404);
Handler vulnerável e corrigido

Colocar o dono na consulta (WHERE id = $1 AND owner_id = $2), ou passar por uma única função de política central, faz com que nenhuma rota possa esquecer a regra.

Duas direções da mesma falha

DireçãoO que o usuário alcançaExemplo
HorizontalDados de outro usuário do mesmo nívelIDOR: ler a fatura de outro cliente
VerticalUma função de um papel mais altoUm usuário comum digita /admin/users e a página responde

Esconder o link "Gerenciar usuários" dos usuários comuns é só navegação. A rota precisa checar o papel por conta própria. Tudo no cliente (páginas, apps, campos ocultos) está sob o controle do usuário. A interface pode esconder coisas por gentileza, mas a decisão de segurança acontece no servidor, a cada requisição, de preferência num único mecanismo reutilizado.

Boas regras

  • Negar por padrão. Recuse tudo, exceto o que uma regra permite explicitamente. Com uma lista de permissão, uma regra esquecida bloqueia um uso legítimo, e alguém reclama logo. Com uma lista de bloqueio, uma regra esquecida expõe dados em silêncio. Você está escolhendo o jeito como o sistema falha.
  • Ids imprevisíveis não são controle de acesso. Trocar números sequenciais por UUIDs aleatórios é um extra útil contra adivinhação, mas um identificador foi feito para circular. Se outra pessoa obtiver o id por qualquer meio, uma checagem ausente ainda a deixa entrar.
  • Cuidado com mass assignment. Se uma rota deixa os usuários editarem o próprio perfil e faz update(session.user.id, body), um usuário pode acrescentar "role": "admin" ao corpo e se promover, mesmo com a posse checada. A autorização vale para os campos também. Valide o corpo com um esquema que liste os campos permitidos e recuse o resto.
  • Escolha o código de recusa de propósito. Um serviço de repositórios privados pode responder 404 em vez de 403 quando o repositório existe mas não é seu, porque um 403 confirma que ele existe. Ambos são recusas válidas; o 404 esconde a existência do recurso. De qualquer forma, a decisão está no servidor e a resposta é consistente, tempo inclusive.
↑ Voltar ao sumário

SSRF, travessia de diretórios e upload de arquivos

Esta seção trata de três jeitos de enganar um servidor para que ele use algo que não deveria: um endereço web, um caminho de arquivo ou um nome de arquivo que veio de fora.

SSRF: o servidor prestativo

Muitos recursos fazem o servidor agir como um navegador: prévia de links, "importar de uma URL", webhooks, buscadores de imagens. Imagine pedir a um funcionário simpático do escritório: "por favor, ligue para este número para mim". Se você escolhe o número, pode pedir que ele ligue para a linha privada do chefe, que só quem está dentro do escritório alcança. No server-side request forgery (SSRF) o usuário fornece uma URL e o servidor a busca de dentro da rede da empresa, então consegue alcançar localhost, serviços internos e o endereço de metadados da nuvem que não têm senha porque "só máquinas internas conseguem conectar". Ele tem categoria própria no OWASP (A10, 2021).

A correção segue a causa: a aplicação precisa decidir para onde aceita se conectar, e fazer a checagem sobre o que a conexão realmente usa, o endereço numérico, em cada salto.

  • Uma lista de bloqueio de texto não funciona. Rejeitar URLs que contêm localhost ou 127.0.0.1 falha, porque o mesmo endereço tem muitas grafias e qualquer nome DNS pode apontar para dentro. Faça o parse da URL, aceite só http e https, resolva o nome e recuse endereços de loopback, privados e link-local. Quando o conjunto de destinos é conhecido, uma lista de permissão de hosts é mais forte que qualquer lista de bloqueio.
  • Redirecionamentos são uma armadilha. Um site permitido pode responder 302 com um Location apontando para um serviço interno, e clientes HTTP seguem redirecionamentos por padrão. Desligue os redirecionamentos automáticos, siga-os você mesmo até um limite pequeno e confira de novo cada novo destino.
  • Checar e usar têm de ser a mesma consulta. Se você resolve o nome, confere, e depois entrega o nome ao cliente e ele resolve de novo, um servidor DNS malicioso pode responder com um endereço público na primeira vez e um interno na segunda (DNS rebinding). Resolva uma vez, valide, conecte-se a esse mesmo endereço e mantenha o nome só para o cabeçalho Host e para o TLS.
  • Adicione regras de rede também. Rode o componente que busca URLs de usuários num segmento isolado ou atrás de um proxy de saída, e autentique entre serviços internos. As checagens da aplicação são uma camada.
  • Esconder a resposta não é correção. A requisição continua sendo feita (SSRF cego).
URL do usuário
  -> parse: esquema é http/https? host na lista de permissão?
  -> resolver o host uma vez -> todos os endereços públicos?
  -> conectar a ESSE endereço (cabeçalho Host = nome)
  -> redirecionou? recomeçar do topo (máx. 3 saltos)
Validação que acompanha a conexão real

Travessia de diretórios: saindo da pasta

Uma rota de download junta a pasta uploads com o nome do arquivo da requisição. Um nome como ../../etc/something usa a sequência .., que significa "suba uma pasta". O sistema operacional a resolve normalmente, e o arquivo lido está fora da pasta de uploads. Isso quebra a suposição de que um nome fica dentro de uma pasta.

// Vulnerável: join confia no nome
const target = path.join(UPLOADS, name);

// Corrigido: valide o RESULTADO, não a entrada
const target = path.resolve(UPLOADS, name);
if (!target.startsWith(UPLOADS + path.sep)) return status(400);
Vulnerável e corrigido

Valide o caminho absoluto final (resolvendo também links simbólicos) e confira que está dentro da raiz permitida. Melhor ainda, nunca aceite caminhos do cliente: receba um identificador e procure-o num índice. Rode o processo com poucas permissões, porque o alcance da falha é tudo o que o processo pode ler.

Upload de arquivos: nome, tipo e conteúdo são alegações

Quando um usuário envia um arquivo, o nome, a extensão e o Content-Type declarado são todos só texto que o cliente escolheu. Um cabeçalho dizendo image/png não prova nada. Um upload seguro combina várias medidas.

MedidaPor quê
Gerar um nome aleatório no servidor, guardando o nome original como metadadoO nome do cliente nunca é usado no sistema de arquivos, então não consegue atravessar nem sobrescrever
Lista de permissão de tipos, e conferir os primeiros bytes (a assinatura do arquivo)O tipo declarado é só uma alegação
Limite de tamanhoEvita encher o disco ou a memória
Guardar fora de pastas executáveis ou servidas diretamente; enviar X-Content-Type-Options: nosniffUm arquivo cheio de HTML não pode ser executado como página ou programa
↑ Voltar ao sumário

Segredos, dependências e a cadeia de suprimentos

Um segredo é qualquer coisa que prova quem você é para outro sistema: uma chave de API, uma senha de banco, uma chave privada. Pense nas chaves da escola. Você não as cola na porta da frente, e não faz uma cópia para cada visitante. Segredos são tratados do mesmo jeito.

Onde os segredos moram

Um segredo é configuração, não código. Ele entra no processo em tempo de execução, vindo de variáveis de ambiente ou de um gerenciador de segredos (um cofre) que controla quem pode lê-lo, registra cada uso e facilita a rotação. O repositório guarda apenas os nomes das variáveis esperadas, nunca os valores.

E se uma pessoa desenvolvedora commita uma chave por engano e depois apaga a linha num novo commit? A chave continua no histórico, e cópias do repositório se espalham rápido. Então a primeira ação eficaz é revogar a chave e emitir uma nova (rotação). Só depois limpe o histórico, verifique se a chave foi usada e evite que aconteça de novo com varredura de segredos antes dos commits.

  1. Revogue a chave vazada e crie uma nova.
  2. Confira nos logs qualquer uso da chave antiga.
  3. Limpe o histórico e as cópias, se possível.
  4. Adicione varredura de segredos antes dos commits para que não aconteça de novo.

Dependências: o código dos outros

A maior parte da sua aplicação é código escrito por outras pessoas: bibliotecas e as bibliotecas dessas bibliotecas. Essa é a cadeia de suprimentos. Duas ideias a protegem.

  • Lockfile. Arquivos como bun.lock ou package-lock.json registram a versão exata e o hash de cada dependência. Sem um, uma faixa como ^1.2.0 instala o que for mais novo naquele dia, e máquinas diferentes podem rodar código diferente. Commite o lockfile e use-o em modo estrito nos builds.
  • Continue verificando, mesmo sem mudar nada. Uma aplicação publicada há um ano com todas as dependências auditadas pode estar vulnerável hoje, porque novas falhas são divulgadas em versões que você já instalou. Saiba o que está instalado (dependências indiretas incluídas), acompanhe avisos de segurança, atualize com frequência e remova o que não usa. Essa é a categoria do OWASP Vulnerable and Outdated Components.
↑ Voltar ao sumário

Juntando tudo: uma falha, um hábito

Todas essas falhas parecem diferentes, mas a maioria compartilha uma de duas raízes. Ou o programa misturou dados com código (injeção, XSS), ou o programa confiou numa alegação de fora (um id, um papel, um nome de arquivo, uma URL, um header de token) sem checar no servidor. A tabela abaixo liga cada falha ao hábito que a barra.

Falha, causa e defesa principal
FalhaCausa raizDefesa principalCamadas extras
Injeção de SQL / comandosDados colados no texto do comandoParâmetros, listas de argumentosValidação, privilégio mínimo, lista de permissão para a estrutura
XSSDados interpretados como marcação ou scriptCodificação na saída, textContentSanitizador, CSP, checagem do esquema da URL
CSRFO navegador envia cookies sozinhoToken anti-CSRFSameSite, sem mudar estado em GET
Senhas fracasHash rápido ou sem saltArgon2id / bcrypt com saltPepper, MFA, limites de tentativas
Roubo / fixação de sessãoId previsível ou reutilizado, servidor mantém a sessão após o logoutId aleatório, id novo no login, logout no servidorHttpOnly, Secure, __Host-, tempos limite
Erros de JWTConfiar no header e pular as claimsAlgoritmo fixado, chave forteChecagens de exp, iss, aud
IDOR / mass assignmentSem checagem de dono ou de campoAutorizar no servidor, a cada requisiçãoNegar por padrão, lista de campos permitidos
SSRFO servidor busca um endereço escolhido pelo usuárioLista de permissão + checagem do endereço resolvidoRedirecionamentos manuais, conexão fixada, regras de saída da rede
Travessia / uploadNome ou tipo do cliente aceitoValidar o caminho resolvido, nome de arquivo aleatórioChecagem da assinatura do conteúdo, nosniff
Segredo vazadoSegredo no repositórioAmbiente ou cofre, rotacionarVarredura de segredos
Componente desatualizadoFalhas conhecidas em bibliotecas antigasLockfile + avisos + atualizaçõesRemover pacotes não usados

Erros comuns a evitar

  • Corrigir o sintoma (banir alguns caracteres) em vez da causa (manter código e dados separados).
  • Esconder coisas na interface e chamar isso de controle de acesso.
  • Usar uma defesa e parar. As camadas falham uma de cada vez; empilhe-as.
  • Tratar o OWASP Top 10 como uma lista completa ou um certificado.
  • Acreditar que cifrar ou aplicar hash a algo é o mesmo que autenticá-lo.
↑ Voltar ao sumário