Pular para o conteúdo
Felipe Túlio

Fundamentos de segurança em sistemas web

19 min de leituraEngenharia de SoftwareProgramação

Segurança de um sistema web é a propriedade de ele continuar se comportando como o projetista pretendia mesmo diante de usuários que tentam fazê-lo agir de outro modo. Isso inclui proteger o que é confidencial, impedir alterações indevidas e manter o serviço disponível. Como uma aplicação web aceita entradas de qualquer pessoa na internet, cada campo de formulário, cabeçalho, parâmetro de URL e requisição de outro site é uma fronteira que precisa ser tratada com desconfiança calibrada (Stuttard; Pinto, 2011).

Este artigo percorre os fundamentos do assunto em quatro movimentos. Primeiro, fixamos os princípios que orientam qualquer decisão de segurança e revisamos autenticação e autorização, a base sobre a qual as demais defesas se apoiam. Depois estudamos três temas que costumam confundir estudantes: CORS, XSS (cross-site scripting) e injeção de SQL. Por fim, tratamos da proteção de dados, em trânsito, em repouso e no ciclo de vida das informações pessoais.

Usaremos um cenário contínuo, o Portal Acadêmico: alunos consultam suas notas, professores lançam notas das próprias turmas e um front-end hospedado em https://portal.exemplo.br conversa com uma API. Todo código é pequeno, em Python 3 (testado na 3.13) ou JavaScript para Node.js (testado na versão 24), e as saídas mostradas foram obtidas na execução real. Os nomes, e-mails e o CPF dos exemplos são fictícios.

Ao final, você deverá ser capaz de:

  • distinguir autenticação de autorização e apontar falhas típicas de cada uma;
  • explicar o que o CORS protege, o que não protege e como configurá-lo com lista de origens permitidas;
  • reconhecer e corrigir XSS por codificação de saída e defesas complementares;
  • impedir injeção de SQL com consultas parametrizadas;
  • escolher mecanismos de proteção de dados e relacioná-los a exigências da LGPD.

Observação sobre o escopo: este texto é introdutório e não substitui um programa de segurança. A revisão de autenticação e autorização é feita aqui de forma autocontida; para uma implementação completa com Node, JWT e MySQL, veja o artigo anterior sobre autenticação e autorização em aplicações web, indicado na própria seção de revisão (Castro, 2026).

Fundamentos: princípios e modelo de ameaça

Antes de estudar ataques específicos, vale adotar uma forma de pensar. A tríade clássica descreve o que queremos preservar: confidencialidade (só quem deve acessa a informação), integridade (a informação não é alterada indevidamente) e disponibilidade (o serviço responde quando necessário). Um ataque de injeção de SQL que lê notas de todos fere a confidencialidade; um que altera notas fere a integridade.

Saltzer e Schroeder (1975) formularam princípios de projeto que seguem atuais. Quatro deles organizam este artigo:

  • Privilégio mínimo: cada usuário e cada componente recebe apenas os acessos de que precisa. Uma conta de banco de dados usada pela API não deveria poder apagar tabelas.
  • Padrão seguro (fail-safe defaults): na dúvida, o acesso é negado. A decisão padrão é “não”; permissões são concedidas explicitamente.
  • Mediação completa: toda tentativa de acesso é verificada, sempre, no servidor. Verificar apenas na interface não conta.
  • Defesa em profundidade: nenhuma camada é perfeita, então combinam-se várias. Veremos que XSS, por exemplo, é mitigado por codificação de saída, política de segurança de conteúdo e cookies protegidos, cada um cobrindo falhas dos outros.

Também é útil pensar em um modelo de ameaça: quem pode atacar, com que capacidades e o que ganharia. No Portal Acadêmico, um aluno curioso pode tentar ver notas de colegas; um site malicioso pode induzir o navegador de uma vítima logada a enviar requisições; um invasor externo pode tentar roubar o banco de dados. Cada defesa a seguir responde a pelo menos uma dessas ameaças.

Uma lista de referência para priorizar riscos é o OWASP Top 10. Na edição de 2021, aparecem, entre outras, as categorias de controle de acesso quebrado, falhas criptográficas, injeção (que inclui o XSS) e falhas de identificação e autenticação (OWASP Foundation, 2021). Isso já indica por que dedicamos tanto espaço a esses temas. A lista é revista periodicamente e pode haver edição mais recente que a citada aqui.

Revisão: autenticação e autorização

Artigo anterior sobre o tema: Castro (2026) mostra, passo a passo, como implementar autenticação e autorização em uma aplicação Express com hash de senha, sessão por JWT em cookie HttpOnly, papéis de usuário e middlewares, em um projeto com banco MySQL. Esta seção revisa os conceitos; o artigo citado aprofunda a implementação.

Autenticação: quem é você?

Autenticação é o processo de verificar a identidade alegada por um usuário ou sistema. O fator mais comum é algo que o usuário sabe (senha); outros são algo que possui (aplicativo gerador de códigos, chave de segurança) e algo que é (biometria). Combinar fatores de naturezas diferentes é a autenticação multifator, que reduz o impacto de uma senha vazada (Grassi et al., 2017).

O ponto crítico no servidor é como guardar senhas. Nunca se armazena a senha em texto claro nem se usa uma função de hash rápida e sem sal. A prática recomendada é aplicar uma função de derivação de chaves deliberadamente custosa, com sal aleatório por usuário, de modo que dois usuários com a mesma senha tenham registros diferentes e que testar milhões de palpites seja caro para o atacante. Funções como scrypt, bcrypt e Argon2 são exemplos desse perfil e costumam ser as indicadas em guias de armazenamento de senhas; a orientação geral de sal e custo deliberado está em Grassi et al. (2017).

O exemplo abaixo usa o scrypt da biblioteca padrão do Python. Os parâmetros de custo (n, r, p) foram escolhidos para a demonstração; em produção, consulte as recomendações atualizadas de armazenamento de senhas e ajuste o custo ao seu hardware. A comparação usa hmac.compare_digest, que leva tempo constante e evita revelar, pelo tempo de resposta, em que posição os valores divergem.

import hashlib
import hmac
import os


def gerar_hash_senha(senha: str) -> str:
    sal = os.urandom(16)
    derivado = hashlib.scrypt(senha.encode("utf-8"), salt=sal, n=2**14, r=8, p=1, dklen=32)
    return sal.hex() + "$" + derivado.hex()


def verificar_senha(senha: str, armazenado: str) -> bool:
    sal_hex, derivado_hex = armazenado.split("$")
    esperado = bytes.fromhex(derivado_hex)
    obtido = hashlib.scrypt(senha.encode("utf-8"), salt=bytes.fromhex(sal_hex), n=2**14, r=8, p=1, dklen=32)
    return hmac.compare_digest(esperado, obtido)


registro_ana = gerar_hash_senha("correto-cavalo-bateria")
registro_bia = gerar_hash_senha("correto-cavalo-bateria")

print("Mesmo texto, registros diferentes:", registro_ana != registro_bia)
print("Senha correta:", verificar_senha("correto-cavalo-bateria", registro_ana))
print("Senha errada:", verificar_senha("123456", registro_ana))

Saída observada:

Mesmo texto, registros diferentes: True
Senha correta: True
Senha errada: False

Ana e Bia poderiam ter a mesma senha, mas os registros guardados diferem por causa do sal; e a verificação aceita apenas a senha correta. Além do armazenamento, as diretrizes atuais de identidade digital recomendam aceitar senhas longas, conferi-las contra listas de senhas comuns ou vazadas e evitar regras que forçam trocas periódicas sem motivo (Grassi et al., 2017).

Depois do login, o servidor precisa lembrar quem é o usuário nas requisições seguintes. Isso costuma ser feito por um identificador de sessão ou um token enviado em cookie. Para cookies de sessão, três atributos merecem hábito: HttpOnly (o JavaScript da página não consegue lê-lo, o que limita o estrago de um XSS), Secure (só trafega por HTTPS) e SameSite (restringe o envio em requisições originadas de outros sites, o que ajuda contra falsificação de requisição entre sites, ou CSRF). Os atributos HttpOnly e Secure constam da especificação de cookies (Barth, 2011a); o SameSite foi padronizado em revisão posterior dela. O artigo anterior de Castro (2026) adota essa abordagem, emitindo o token de sessão em cookie HttpOnly. Protocolos de delegação como o OAuth 2.0 resolvem outro problema, o de permitir que um aplicativo acesse recursos em nome do usuário sem conhecer sua senha (Hardt, 2012), e não dispensam o servidor de decidir o que cada requisição pode fazer.

Autorização: o que você pode fazer?

Autorização decide se uma identidade já autenticada pode executar certa ação sobre certo recurso. Autenticar sem autorizar é como conferir o crachá na portaria e deixar todas as salas abertas. A falha mais comum é o controle de acesso quebrado, que lidera o OWASP Top 10:2021 (OWASP Foundation, 2021). Um caso típico é a referência direta insegura a objeto (IDOR): a API devolve /notas?aluno=bia a qualquer usuário logado, bastando trocar o parâmetro.

Há dois modelos úteis para começar. No controle baseado em papéis (RBAC), permissões são ligadas a papéis, como aluno e professor. No controle baseado em atributos ou relações, a decisão considera também o recurso: este aluno é o dono da nota? este professor leciona nesta turma? Em sistemas reais, quase sempre combinamos os dois, como no código abaixo.

PERMISSOES = {
    "aluno": {"ler_propria_nota"},
    "professor": {"ler_nota_da_turma", "lancar_nota"},
}

NOTAS = {("ana", "BD1"): 8.5, ("bia", "BD1"): 6.0}
TURMAS = {"BD1": {"prof_carlos"}}


def pode_ler_nota(usuario: str, papel: str, aluno_alvo: str, disciplina: str) -> bool:
    permissoes = PERMISSOES.get(papel, set())
    if "ler_propria_nota" in permissoes and usuario == aluno_alvo:
        return True
    if "ler_nota_da_turma" in permissoes and usuario in TURMAS.get(disciplina, set()):
        return True
    return False


print(pode_ler_nota("ana", "aluno", "ana", "BD1"))
print(pode_ler_nota("ana", "aluno", "bia", "BD1"))
print(pode_ler_nota("prof_carlos", "professor", "bia", "BD1"))
print(pode_ler_nota("prof_rita", "professor", "bia", "BD1"))

Saída observada:

True
False
True
False

As quatro linhas correspondem às quatro chamadas: Ana lê a própria nota (permitido); Ana tenta ler a de Bia (negado); o professor Carlos, que leciona BD1, lê a nota de Bia (permitido); a professora Rita, que não leciona BD1, é negada. Observe o padrão seguro: a função só retorna True quando alguma regra explicitamente autoriza; todo o resto cai em False. Castro (2026) implementa a verificação por papel como middleware de autorização, que responde 401 ou 403 conforme o caso; aqui acrescentamos a verificação por relação com o recurso. Esse tipo de verificação precisa ocorrer no servidor, a cada requisição, para o recurso específico. Esconder o botão no front-end não é controle de acesso.

A figura mostra a ordem lógica dessas camadas no tratamento de uma requisição. Identificar vem antes de decidir, e ambas vêm antes de tocar nos dados.

Figura 1. Caminho de uma requisição no Portal Acadêmico: autenticação, autorização, validação e acesso a dados.

flowchart TD
    A["Requisição recebida"] --> B{"Identidade válida?"}
    B -->|não| C["401 Não autenticado"]
    B -->|sim| D{"Ação permitida para este recurso?"}
    D -->|não| E["403 Proibido"]
    D -->|sim| F["Validar entrada"]
    F --> G["Consulta parametrizada"]
    G --> H["Resposta com saída codificada"]

Os códigos de estado HTTP 401 (não autenticado) e 403 (autenticado, mas sem permissão) refletem a distinção entre as duas etapas. As próximas seções tratam justamente das caixas “Validar entrada”, “Consulta parametrizada” e “Saída codificada”, além da política que o navegador aplica entre sites.

CORS: o que o navegador permite entre origens

A política de mesma origem

Uma origem é a tripla formada por esquema, host e porta, como https://portal.exemplo.br:443 (Barth, 2011b). Por padrão, navegadores aplicam a política de mesma origem: o JavaScript de uma página não pode ler respostas de outra origem. Sem essa regra, qualquer página que você abrisse poderia ler seu e-mail em outra aba, aproveitando seu login.

O CORS (Cross-Origin Resource Sharing) é o mecanismo, descrito no Fetch Standard, pelo qual um servidor relaxa de forma controlada essa restrição, declarando por cabeçalhos HTTP quais outras origens podem ler suas respostas (WHATWG, [20—]). Se o front-end em https://portal.exemplo.br chama a API em https://api.exemplo.br, as origens diferem e a API precisa autorizá-la explicitamente.

Como funciona

Para requisições simples, o navegador envia o cabeçalho Origin e examina a resposta: se o Access-Control-Allow-Origin corresponder à origem da página, o JavaScript pode ler o corpo; senão, o navegador bloqueia a leitura. Para requisições que usam métodos ou cabeçalhos fora do conjunto “simples”, como PUT, DELETE ou Content-Type: application/json, o navegador envia antes uma requisição de preflight (OPTIONS), perguntando o que é permitido.

Figura 2. Preflight CORS entre o front-end do portal e a API.

sequenceDiagram
    participant N as Navegador (portal.exemplo.br)
    participant A as API (api.exemplo.br)
    N->>A: OPTIONS /notas com Origin e método solicitado
    A-->>N: 204 com Allow-Origin, Allow-Methods e Allow-Headers
    N->>A: POST /notas (requisição real)
    A-->>N: 200 com Allow-Origin e o corpo da resposta

No diagrama, a API só vê a requisição real se o preflight passar. Ainda assim, a resposta real também precisa trazer Access-Control-Allow-Origin, caso contrário o navegador não entregará o corpo ao JavaScript.

O código abaixo implementa uma API mínima com uma lista de origens permitidas. Ele ecoa a origem apenas quando ela está na lista e acrescenta Vary: Origin, para que caches não reutilizem uma resposta destinada a outra origem. Em seguida, faz duas chamadas de teste com origens diferentes.

const http = require("node:http");

const ORIGENS_PERMITIDAS = new Set(["https://portal.exemplo.br"]);

const servidor = http.createServer((req, res) => {
  const origem = req.headers.origin;
  if (origem && ORIGENS_PERMITIDAS.has(origem)) {
    res.setHeader("Access-Control-Allow-Origin", origem);
    res.setHeader("Vary", "Origin");
  }
  if (req.method === "OPTIONS") {
    res.setHeader("Access-Control-Allow-Methods", "GET, POST");
    res.setHeader("Access-Control-Allow-Headers", "Content-Type");
    res.setHeader("Access-Control-Max-Age", "600");
    res.writeHead(204).end();
    return;
  }
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ nota: 8.5 }));
});

async function testar(origem) {
  const { port } = servidor.address();
  const resposta = await fetch(`http://127.0.0.1:${port}/notas`, {
    headers: { Origin: origem },
  });
  const cabecalho = resposta.headers.get("access-control-allow-origin");
  console.log(`Origin ${origem} -> Access-Control-Allow-Origin: ${cabecalho}`);
}

servidor.listen(0, async () => {
  await testar("https://portal.exemplo.br");
  await testar("https://site-malicioso.example");
  servidor.close();
});

Saída observada:

Origin https://portal.exemplo.br -> Access-Control-Allow-Origin: https://portal.exemplo.br
Origin https://site-malicioso.example -> Access-Control-Allow-Origin: null

O null impresso na segunda linha significa que o cabeçalho não veio na resposta (headers.get devolve null para cabeçalho ausente), e não que a origem seja a string “null”. Como o Node não aplica a política do navegador, a requisição chega ao servidor e a resposta é lida pelo script de teste; num navegador real, a ausência do cabeçalho faria o navegador bloquear a leitura pelo site malicioso.

Erros comuns e limites do CORS

  • CORS não é autenticação nem autorização. Ele só afeta o que o navegador deixa um script ler. Qualquer cliente fora do navegador, como curl, ignora o CORS. A API continua precisando de autenticação e autorização no servidor.
  • O CORS não impede o envio de certas requisições. Uma requisição “simples” de outro site pode chegar ao servidor e produzir efeitos, mesmo que o navegador não entregue a resposta. Por isso defesas contra CSRF (cookies SameSite, tokens) são necessárias para operações que alteram estado.
  • Evite Access-Control-Allow-Origin: * em APIs que usam credenciais. Com requisições com credenciais (cookies), o navegador não aceita o curinga; e a saída comum, refletir qualquer Origin recebido e ainda permitir credenciais, equivale a desligar a proteção. Use lista fixa.
  • Use Vary: Origin quando a resposta varia com a origem, para não envenenar caches.

XSS: quando o site executa o código do atacante

Cross-site scripting (XSS) ocorre quando a aplicação inclui dados controlados por um atacante em uma página sem tratá-los adequadamente, e o navegador interpreta esses dados como código. Como o script injetado roda na origem do site vulnerável, ele passa pela política de mesma origem e pode ler dados da página, enviar requisições em nome do usuário e, se os cookies não forem HttpOnly, roubar a sessão (Stuttard; Pinto, 2011).

Distinguem-se três variantes: armazenado (o conteúdo malicioso fica salvo, por exemplo, em um comentário, e atinge todos que o veem), refletido (o conteúdo vem da própria requisição, como um parâmetro de busca, e é devolvido na resposta) e baseado em DOM (o próprio JavaScript do cliente insere dados não confiáveis na página, por exemplo via innerHTML).

No Portal Acadêmico, imagine um mural de comentários da turma. Se um aluno enviar <img src=x onerror="alert(document.cookie)"> e o servidor inserir o texto cru no HTML, o navegador de cada colega tentará carregar a imagem inexistente, falhará e executará o código do atributo onerror.

A defesa central é a codificação de saída sensível ao contexto: converter os caracteres especiais do contexto em que o dado será inserido. No corpo do HTML e em atributos entre aspas, isso significa trocar &, <, >, " e ' por entidades.

const ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };

function escaparHtml(texto) {
  return String(texto).replace(/[&<>"']/g, (c) => ESCAPES[c]);
}

const comentario = '<img src=x onerror="alert(document.cookie)">';

console.log("Sem tratamento:", `<p>${comentario}</p>`);
console.log("Com escape:    ", `<p>${escaparHtml(comentario)}</p>`);

// Contexto de atributo: o escape também cobre aspas.
const nome = '" onmouseover="roubar()';
console.log("Atributo:      ", `<input value="${escaparHtml(nome)}">`);

Saída observada:

Sem tratamento: <p><img src=x onerror="alert(document.cookie)"></p>
Com escape:     <p>&lt;img src=x onerror=&quot;alert(document.cookie)&quot;&gt;</p>
Atributo:       <input value="&quot; onmouseover=&quot;roubar()">

Na primeira linha, o HTML resultante contém uma tag img executável. Na segunda, o texto vira entidades e o navegador o exibe como texto, sem interpretá-lo. Na terceira, o escape das aspas impede que o valor “saia” do atributo para criar um novo atributo de evento.

Essa função cobre apenas HTML e atributos entre aspas. Cada contexto tem regras próprias: dados inseridos dentro de um <script>, de uma URL (por exemplo, javascript: em href) ou de CSS exigem codificação diferente, e a regra mais segura é evitar inserir dados não confiáveis nesses locais. Na prática, prefira mecanismos que fazem a codificação automaticamente: motores de template com escape ativado por padrão, frameworks de interface que tratam texto como texto e, no DOM, textContent em vez de innerHTML.

Como defesa em profundidade, somam-se:

  • Content Security Policy (CSP): cabeçalho que limita de onde scripts podem ser carregados e, bem configurado, bloqueia a execução de scripts em linha, reduzindo o dano de uma falha de escape;
  • Cookies HttpOnly: mesmo com XSS, o script não lê o cookie de sessão (Barth, 2011a);
  • Validação de entrada: ajuda (por exemplo, um campo de nota aceita só números), mas não substitui a codificação de saída, porque há campos legítimos de texto livre, como comentários.

Injeção de SQL: quando dados viram comandos

A injeção de SQL acontece quando a aplicação monta um comando SQL concatenando texto fornecido pelo usuário, de modo que a entrada altera a estrutura da consulta em vez de ser tratada como simples valor (Stuttard; Pinto, 2011). É uma das formas de injeção, categoria que o OWASP Top 10:2021 mantém entre os riscos mais relevantes (OWASP Foundation, 2021).

No Portal Acadêmico, uma tela de busca recebe um e-mail e consulta a tabela de usuários. O código abaixo traz duas versões: a vulnerável, que concatena, e a segura, que usa um parâmetro (?). Para o teste, a entrada maliciosa ' OR '1'='1 torna a condição sempre verdadeira.

import sqlite3

conexao = sqlite3.connect(":memory:")
conexao.execute("CREATE TABLE usuario (email TEXT PRIMARY KEY, nome TEXT, papel TEXT)")
conexao.executemany(
    "INSERT INTO usuario VALUES (?, ?, ?)",
    [("ana@exemplo.br", "Ana", "aluno"), ("carlos@exemplo.br", "Carlos", "professor")],
)


def buscar_vulneravel(email: str):
    consulta = "SELECT nome, papel FROM usuario WHERE email = '" + email + "'"
    return consulta, conexao.execute(consulta).fetchall()


def buscar_seguro(email: str):
    consulta = "SELECT nome, papel FROM usuario WHERE email = ?"
    return consulta, conexao.execute(consulta, (email,)).fetchall()


entrada_maliciosa = "' OR '1'='1"

consulta, linhas = buscar_vulneravel(entrada_maliciosa)
print("Vulnerável:", consulta)
print("Linhas:", linhas)

consulta, linhas = buscar_seguro(entrada_maliciosa)
print("Parametrizada:", consulta)
print("Linhas:", linhas)

Saída observada:

Vulnerável: SELECT nome, papel FROM usuario WHERE email = '' OR '1'='1'
Linhas: [('Ana', 'aluno'), ('Carlos', 'professor')]
Parametrizada: SELECT nome, papel FROM usuario WHERE email = ?
Linhas: []

Na versão vulnerável, as aspas da entrada fecharam o literal e o OR '1'='1' virou parte do comando: a consulta devolveu todos os usuários. Na versão parametrizada, o texto do comando e o valor viajam separados; o banco trata a entrada como um e-mail e, como nenhum usuário tem esse e-mail, devolve zero linhas. Esse é o princípio da correção: separar código de dados.

Boas práticas, em ordem de importância:

  1. Consultas parametrizadas (ou prepared statements) em todo acesso ao banco, inclusive em ORMs, cujos métodos de SQL cru continuam vulneráveis se receberem concatenação.
  2. Lista de permissões para partes não parametrizáveis. Nomes de tabelas, colunas e a direção de ORDER BY não podem ser parâmetros; se vierem do usuário, compare com uma lista fixa de valores aceitos.
  3. Privilégio mínimo no banco: a conta da aplicação não deve ser dona do esquema nem ter permissão de administração. Assim, mesmo uma falha tem alcance limitado.
  4. Mensagens de erro genéricas ao usuário, sem expor consultas ou detalhes do banco; o detalhe vai para o log.
  5. Validação de entrada como camada complementar, nunca como única defesa. Remover aspas manualmente (“sanitizar”) é frágil e não se recomenda.

Proteção de dados

Em trânsito, em repouso e em uso

A proteção de dados começa por saber quais dados existem e quão sensíveis são. Dados pessoais, como CPF, e-mail e notas, exigem cuidado maior que dados públicos. O princípio mais barato é a minimização: o dado que não foi coletado não pode vazar. Convém dividir o ciclo de vida em três estados:

  • Em trânsito: use sempre HTTPS. O TLS 1.3 fornece confidencialidade, integridade e autenticação do servidor entre navegador e API (Rescorla, 2018). Sem TLS, qualquer ponto da rede pode ler ou alterar tráfego, inclusive cookies e senhas.
  • Em repouso: discos, bancos e cópias de segurança podem ser perdidos ou copiados. Criptografar campos sensíveis ou o volume reduz o impacto desse vazamento. Senhas são caso à parte: usam hash com sal, como visto, e não criptografia reversível.
  • Em uso: logs, mensagens de erro, ferramentas de análise e ambientes de teste também vazam dados. Evite registrar senhas, tokens e documentos completos, e use dados fictícios ou mascarados fora de produção.

Criptografia autenticada

Ao criptografar dados que precisam ser lidos de novo, use um modo de criptografia autenticada, como o AES-GCM, que além de manter o segredo detecta adulteração. O código abaixo cifra um CPF fictício com AES-256-GCM, usando um vetor de inicialização (IV) aleatório novo a cada mensagem, e depois altera um bit do texto cifrado para mostrar que a alteração é detectada.

const crypto = require("node:crypto");

const chave = crypto.randomBytes(32);

function cifrar(texto) {
  const iv = crypto.randomBytes(12);
  const cifra = crypto.createCipheriv("aes-256-gcm", chave, iv);
  const dados = Buffer.concat([cifra.update(texto, "utf8"), cifra.final()]);
  return { iv, dados, etiqueta: cifra.getAuthTag() };
}

function decifrar({ iv, dados, etiqueta }) {
  const decifra = crypto.createDecipheriv("aes-256-gcm", chave, iv);
  decifra.setAuthTag(etiqueta);
  return Buffer.concat([decifra.update(dados), decifra.final()]).toString("utf8");
}

const pacote = cifrar("CPF 123.456.789-09");
console.log("Recuperado:", decifrar(pacote));

pacote.dados[0] ^= 1; // adulteração de um único bit
try {
  decifrar(pacote);
} catch (erro) {
  console.log("Adulteração detectada:", erro.message);
}

Saída observada:

Recuperado: CPF 123.456.789-09
Adulteração detectada: Unsupported state or unable to authenticate data

A primeira linha confirma a decifragem; a segunda mostra que a alteração de um único bit faz a verificação da etiqueta de autenticação falhar. Três cuidados não aparecem no exemplo, mas são decisivos. Primeiro, nunca reutilize o par chave e IV no GCM. Segundo, a chave do exemplo é gerada na memória e some ao fim do programa; em sistemas reais, ela deve ficar em um serviço de gestão de chaves ou cofre de segredos, separada dos dados, com rotação e controle de acesso. Terceiro, não crie algoritmos próprios: use bibliotecas consolidadas e parâmetros atuais.

Aspectos legais: a LGPD

No Brasil, a Lei Geral de Proteção de Dados Pessoais (Lei nº 13.709/2018) estabelece, entre seus princípios, a necessidade (limitar o tratamento ao mínimo necessário) e a segurança (uso de medidas técnicas e administrativas para proteger os dados). O art. 46 determina que os agentes de tratamento adotem medidas de segurança aptas a proteger dados pessoais contra acessos não autorizados e contra situações acidentais ou ilícitas de destruição, perda, alteração, comunicação ou tratamento inadequado (Brasil, 2018). Em termos práticos, o conteúdo desta seção, junto com o controle de acesso e a prevenção de injeção e XSS, é o que dá concretude a essa exigência. A lei também prevê outros pontos, como bases legais e direitos dos titulares, que ultrapassam o escopo técnico deste texto; para um caso real, consulte a versão vigente da norma e orientação jurídica.

Limitações e visão de conjunto

Os exemplos foram reduzidos para fins didáticos. O de senhas não trata de limitação de tentativas, bloqueio de contas nem recuperação de senha. O de autorização usa dicionários em memória, quando em produção as regras estariam em banco e seriam testadas com casos de negação. O servidor CORS não mostra credenciais. A função de escape não cobre todos os contextos. A criptografia não resolve a gestão de chaves. Nenhuma dessas peças, isoladamente, torna o sistema “seguro”.

O que os exemplos mostram é um padrão: em cada ameaça há uma fronteira onde dados não confiáveis cruzam para dentro de um contexto que os interpreta, e a defesa correta é tratar o dado conforme o contexto (parâmetro no SQL, entidade no HTML, lista de origens no CORS, verificação de dono na autorização). Somada à defesa em profundidade, essa regra explica a maior parte das vulnerabilidades web comuns. Lembre-se também de que a segurança é contínua: dependências desatualizadas, configurações e novas funcionalidades reabrem riscos, e testes de segurança e revisão de código fazem parte do ciclo de desenvolvimento.

Conclusão

Segurança web é decidir, em cada fronteira do sistema, o que é confiável. Autenticação responde “quem é?” e autorização responde “o que pode?”; ambas ocorrem no servidor e seguem os princípios de privilégio mínimo e padrão seguro. O CORS controla o que o navegador deixa um site ler de outro, mas não protege a API de clientes que o ignoram. XSS e injeção de SQL têm a mesma raiz, dados tratados como código, e a mesma cura: codificação de saída e consultas parametrizadas, apoiadas por CSP, cookies protegidos e privilégio mínimo no banco. Por fim, a proteção de dados combina minimização, TLS, criptografia autenticada com boa gestão de chaves e aderência à LGPD. Aplicadas em camadas, essas práticas tornam um ataque bem-sucedido muito mais difícil e muito menos danoso.

Referências

BARTH, A. HTTP State Management Mechanism. RFC 6265. [S. l.]: Internet Engineering Task Force, 2011a.

BARTH, A. The Web Origin Concept. RFC 6454. [S. l.]: Internet Engineering Task Force, 2011b.

BRASIL. Lei nº 13.709, de 14 de agosto de 2018. Lei Geral de Proteção de Dados Pessoais (LGPD). Brasília, DF: Presidência da República, 2018.

CASTRO, Felipe Túlio de. Autenticação e autorização em aplicações web com Node, JWT e MySQL. [S. l.]: [s. n.], 23 set. 2026. Disponível em: https://felipetulio.com.br/autenticacao-e-autorizacao-em-aplicacoes-web-com-node-jwt-e-mysql/. Acesso em: 31 out. 2026.

GRASSI, Paul A. et al. Digital identity guidelines: authentication and lifecycle management. NIST Special Publication 800-63B. Gaithersburg: National Institute of Standards and Technology, 2017.

HARDT, D. (ed.). The OAuth 2.0 Authorization Framework. RFC 6749. [S. l.]: Internet Engineering Task Force, 2012.

OWASP FOUNDATION. OWASP Top 10:2021. [S. l.]: OWASP Foundation, 2021.

RESCORLA, E. The Transport Layer Security (TLS) Protocol Version 1.3. RFC 8446. [S. l.]: Internet Engineering Task Force, 2018.

SALTZER, Jerome H.; SCHROEDER, Michael D. The protection of information in computer systems. Proceedings of the IEEE, v. 63, n. 9, p. 1278-1308, 1975.

STUTTARD, Dafydd; PINTO, Marcus. The web application hacker’s handbook: finding and exploiting security flaws. 2. ed. Indianapolis: Wiley, 2011.

WHATWG. Fetch Standard. [S. l.]: WHATWG, [20—].

Atividade de fixação

O Portal Acadêmico ganha um endpoint GET /api/notas?aluno=<id>&ordenar=<coluna> consumido pelo front-end em https://portal.exemplo.br. O código atual (a) devolve a nota de qualquer aluno a qualquer usuário autenticado, (b) monta o ORDER BY concatenando o parâmetro ordenar, (c) responde com Access-Control-Allow-Origin igual ao Origin recebido, junto com Access-Control-Allow-Credentials: true, e (d) exibe no mural o nome do aluno sem escape. Para cada item, identifique a vulnerabilidade, explique o que um atacante conseguiria e proponha a correção, indicando qual dos princípios de Saltzer e Schroeder (1975) ela aplica.