Segurança não é um recurso que você adiciona ao final do projeto — é uma camada que permeia cada decisão de desenvolvimento. Uma aplicação insegura pode expor dados pessoais de usuários, permitir que atacantes assumam contas, ou destruir anos de trabalho em minutos.
A boa notícia é que a maioria dos ataques explora vulnerabilidades bem conhecidas e documentadas. Entender como esses ataques funcionam é o primeiro passo para se defender deles. A OWASP (Open Web Application Security Project) mantém uma lista atualizada das dez vulnerabilidades mais críticas em aplicações web — este artigo cobre as mais relevantes para desenvolvedores JavaScript.
1. Injeção — SQL, NoSQL e Command Injection
Ataques de injeção acontecem quando dados fornecidos pelo usuário são interpretados como comandos pelo sistema. É a vulnerabilidade mais antiga e ainda devastadoramente comum.
O princípio do ataque é sempre o mesmo: o atacante insere caracteres especiais em um campo de formulário ou parâmetro de URL que alteram a lógica de uma consulta ou comando.
// ── INJEÇÃO EM MONGODB (NoSQL Injection) ───────────
// O Mongoose protege automaticamente contra a maioria dos casos,
// mas é importante entender o que pode acontecer sem proteção.
// ❌ VULNERÁVEL — confia cegamente nos dados recebidos
app.post('/login', async (req, res) => {
const { email, senha } = req.body;
// Se o atacante enviar: { "email": { "$gt": "" }, "senha": { "$gt": "" } }
// O MongoDB interpreta { "$gt": "" } como "maior que string vazia"
// isso retorna QUALQUER usuário do banco — autenticação bypassada!
const usuario = await Usuario.findOne({ email, senha });
if (usuario) res.json({ token: gerarToken(usuario) });
});
// ✅ SEGURO — valida tipos antes de usar
app.post('/login', async (req, res) => {
const { email, senha } = req.body;
// Verifica que os valores são strings — operadores MongoDB são objetos
if (typeof email !== 'string' || typeof senha !== 'string') {
return res.status(400).json({ erro: 'Formato de dados inválido.' });
}
// Sanitiza removendo caracteres que não pertencem a um email
const emailLimpo = email.trim().toLowerCase();
// Nunca compara senha em texto puro no banco — usa bcrypt
const usuario = await Usuario.findOne({ email: emailLimpo }).select('+senha');
if (!usuario) return res.status(401).json({ erro: 'Credenciais inválidas.' });
const senhaCorreta = await bcrypt.compare(senha, usuario.senha);
if (!senhaCorreta) return res.status(401).json({ erro: 'Credenciais inválidas.' });
res.json({ token: gerarToken(usuario._id) });
});
// ── COMMAND INJECTION ───────────────────────────────
// Nunca execute comandos do sistema com dados do usuário
// ❌ CATASTRÓFICO — permite executar qualquer comando no servidor
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
const host = req.query.host;
// Atacante envia: host=google.com; rm -rf /
// O servidor executa: ping google.com; rm -rf /
exec(`ping -c 1 ${host}`, (erro, stdout) => res.send(stdout));
});
// ✅ SEGURO — valida estritamente o input antes de qualquer operação
app.get('/ping', (req, res) => {
const host = req.query.host;
// Permite apenas caracteres válidos em hostnames
// Rejeita qualquer tentativa de injetar comandos
const hostValido = /^[a-zA-Z0-9.-]{1,253}$/.test(host);
if (!hostValido) {
return res.status(400).json({ erro: 'Host inválido.' });
}
// Passa o argumento como array — não como string concatenada
// Isso impede interpretação de caracteres especiais do shell
const { execFile } = require('child_process');
execFile('ping', ['-c', '1', host], (erro, stdout) => {
if (erro) return res.status(500).json({ erro: 'Falha no ping.' });
res.send(stdout);
});
});
Regra de ouro contra injeção: trate todo dado externo como não confiável. Valide tipos, use parâmetros em vez de concatenação de strings, e nunca construa comandos com dados do usuário.
2. Cross-Site Scripting (XSS)
XSS acontece quando um atacante consegue inserir JavaScript malicioso em uma página que será executado no navegador de outras pessoas. É a vulnerabilidade mais comum em aplicações web.
Imagine um campo de comentários onde o atacante digita . Se o site exibir esse comentário sem sanitizar, o script vai roubar os cookies de todos que visualizarem a página.
// ── XSS NO BACK-END ─────────────────────────────────
// ❌ VULNERÁVEL — retorna HTML sem sanitizar dados do banco
app.get('/perfil/:id', async (req, res) => {
const usuario = await Usuario.findById(req.params.id);
// Se usuario.bio contiver <script>..., será executado no navegador
res.send(`<div class="bio">${usuario.bio}</div>`);
});
// ✅ SEGURO — use um sanitizador de HTML ou evite HTML dinâmico
// npm install dompurify jsdom
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
app.get('/perfil/:id', async (req, res) => {
const usuario = await Usuario.findById(req.params.id);
// Remove tags e atributos perigosos, mantém HTML formatado legítimo
const bioSegura = DOMPurify.sanitize(usuario.bio, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br'],
ALLOWED_ATTR: [], // nenhum atributo permitido (evita onclick, etc.)
});
res.send(`<div class="bio">${bioSegura}</div>`);
});
// ── HEADERS DE SEGURANÇA COM HELMET ─────────────────
// O Helmet configura automaticamente vários headers que protegem contra XSS
// npm install helmet
const helmet = require('helmet');
app.use(helmet());
// Content-Security-Policy: define quais scripts podem executar
// Isso é a defesa em profundidade contra XSS — mesmo se injetar script,
// o navegador bloqueia a execução se não vier de fonte aprovada
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"], // só recursos do próprio domínio
scriptSrc: ["'self'"], // sem scripts inline, sem CDNs não listados
styleSrc: ["'self'", "'unsafe-inline'"], // permite estilos inline (comum em React)
imgSrc: ["'self'", 'data:', 'https:'], // imagens do próprio domínio ou https
connectSrc: ["'self'", process.env.API_URL], // conexões apenas para a API
fontSrc: ["'self'", 'https://fonts.googleapis.com'],
objectSrc: ["'none'"], // bloqueia plugins (Flash, etc.)
upgradeInsecureRequests: [], // força HTTPS
},
})
);
// ── XSS NO FRONT-END REACT ──────────────────────────
// O React escapa automaticamente o conteúdo inserido via JSX — isso já é
// uma defesa poderosa. O problema aparece quando usamos dangerouslySetInnerHTML.
// ❌ PERIGOSO — executa qualquer HTML/script do banco
function Comentario({ texto }) {
// Se texto contiver <script>alert('xss')</script>, vai executar
return <div dangerouslySetInnerHTML={{ __html: texto }} />;
}
// ✅ SEGURO — sanitiza antes de renderizar HTML
// npm install dompurify
import DOMPurify from 'dompurify';
function Comentario({ texto }) {
// DOMPurify remove tags e atributos perigosos antes de renderizar
const textoSeguro = DOMPurify.sanitize(texto, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
return <div dangerouslySetInnerHTML={{ __html: textoSeguro }} />;
}
// ✅ MELHOR AINDA — evite dangerouslySetInnerHTML sempre que possível
// Se o texto é plain text, o React já escapa automaticamente:
function Comentario({ texto }) {
return <p>{texto}</p>; // '{texto}' é sempre escapado pelo React
}
3. Autenticação e Gerenciamento de Sessão
Erros de autenticação são particularmente graves porque dão ao atacante acesso total à conta do usuário. Vamos cobrir as práticas essenciais.
// ── JWT — configuração segura ────────────────────────
const jwt = require('jsonwebtoken');
// ❌ JWT inseguro — chave fraca, algoritmo padrão sem verificação
const tokenInseguro = jwt.sign({ userId: 1 }, 'senha123');
// ✅ JWT seguro — chave forte, expiração, algoritmo explícito
function gerarToken(userId) {
return jwt.sign(
{
id: userId,
// iat (issued at) é adicionado automaticamente pelo jwt.sign
},
process.env.JWT_SECRET, // mínimo 256 bits — gere com crypto.randomBytes(64)
{
expiresIn: '15m', // tokens de acesso devem ser de curta duração
algorithm: 'HS256', // sempre especifique o algoritmo explicitamente
issuer: 'api-gestao', // identifica quem emitiu o token
audience: 'app-gestao', // identifica para quem o token é destinado
}
);
}
// Token de refresh — vida longa, armazenado com mais segurança
function gerarRefreshToken(userId) {
return jwt.sign(
{ id: userId, tipo: 'refresh' },
process.env.JWT_REFRESH_SECRET, // chave diferente do access token
{ expiresIn: '7d' }
);
}
// Verificação robusta do token
function verificarToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256'], // rejeita tokens com algoritmo diferente
issuer: 'api-gestao',
audience: 'app-gestao',
});
} catch (erro) {
if (erro.name === 'TokenExpiredError') {
throw new Error('Token expirado. Faça login novamente.');
}
if (erro.name === 'JsonWebTokenError') {
throw new Error('Token inválido.');
}
throw erro;
}
}
// ── SENHAS — armazenamento seguro ────────────────────
const bcrypt = require('bcrypt');
// ❌ NUNCA armazene senhas em texto puro ou com hash fraco (MD5, SHA1)
const senhaInsegura = md5(senha); // MD5 é completamente quebrável
// ✅ Use bcrypt com fator de custo adequado
// Fator 12 é o padrão atual — leva ~250ms por hash (dificulta força bruta)
// Aumente conforme o hardware melhorar: 13 ou 14 em servidores modernos
async function hashSenha(senhaTexto) {
const saltRounds = 12;
return bcrypt.hash(senhaTexto, saltRounds);
}
async function verificarSenha(senhaTexto, senhaHash) {
return bcrypt.compare(senhaTexto, senhaHash);
}
// ── PROTEÇÃO CONTRA TIMING ATTACKS ──────────────────
// Nunca retorne erros diferentes para "usuário não existe" vs "senha errada"
// Isso permite que atacantes descubram quais emails estão cadastrados
// ❌ Vaza informação sobre usuários existentes
app.post('/login', async (req, res) => {
const usuario = await Usuario.findOne({ email: req.body.email });
if (!usuario) return res.status(401).json({ erro: 'Usuário não encontrado.' }); // ❌
const ok = await bcrypt.compare(req.body.senha, usuario.senha);
if (!ok) return res.status(401).json({ erro: 'Senha incorreta.' }); // ❌
});
// ✅ Mesma mensagem para ambos os casos
app.post('/login', async (req, res) => {
const usuario = await Usuario.findOne({ email: req.body.email }).select('+senha');
// Faz o compare mesmo se o usuário não existir
// Isso garante tempo de resposta constante — evita timing attacks
const senhaFalsa = '$2b$12$invalidhashforcomparison.....'; // hash inválido
const senhaHash = usuario?.senha || senhaFalsa;
const senhaCorreta = await bcrypt.compare(req.body.senha, senhaHash);
if (!usuario || !senhaCorreta) {
return res.status(401).json({ erro: 'Credenciais inválidas.' }); // ✅ mesma msg
}
res.json({ token: gerarToken(usuario._id) });
});
4. Rate Limiting — proteção contra força bruta
Rate limiting limita quantas requisições um mesmo IP pode fazer em um período. Sem isso, um atacante pode tentar milhões de combinações de senha por hora.
// npm install express-rate-limit
const rateLimit = require('express-rate-limit');
// Limite global — aplica a toda a API
// 100 requisições por 15 minutos por IP
const limiteGeral = rateLimit({
windowMs: 15 * 60 * 1000, // janela de 15 minutos
max: 100, // máximo de requisições por janela
standardHeaders: true, // inclui headers RateLimit-* na resposta
legacyHeaders: false, // não inclui headers X-RateLimit-* deprecados
message: {
erro: 'Muitas requisições. Tente novamente em 15 minutos.',
status: 429,
},
});
// Limite específico para autenticação — muito mais restritivo
// 5 tentativas de login por 15 minutos por IP
const limiteAuth = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true, // logins bem-sucedidos não contam no limite
message: {
erro: 'Muitas tentativas de login. Tente novamente em 15 minutos.',
status: 429,
},
});
// Aplicando os limitadores
app.use(limiteGeral); // toda a API
app.use('/auth/login', limiteAuth); // apenas login
app.use('/auth/registrar', limiteAuth); // e registro
app.use('/auth/esqueci-senha', limiteAuth); // e recuperação de senha
5. CORS — configuração correta
CORS (Cross-Origin Resource Sharing) define quais domínios podem fazer requisições para sua API. Uma configuração incorreta pode expor sua API a qualquer site na internet.
// npm install cors
const cors = require('cors');
// ❌ CORS aberto demais — qualquer site do mundo acessa sua API
app.use(cors()); // aceita qualquer origem — nunca faça isso em produção
// ✅ CORS configurado corretamente
const origensPermitidas = [
'https://meuapp.vercel.app',
'https://www.meuapp.com.br',
// Em desenvolvimento, adiciona o localhost
...(process.env.NODE_ENV === 'development' ? ['http://localhost:5173'] : []),
];
app.use(
cors({
origin: (origin, callback) => {
// Permite requisições sem origin (ex: curl, Postman, apps mobile)
// Em produção você pode querer bloquear isso também
if (!origin || origensPermitidas.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`Origem não permitida pelo CORS: ${origin}`));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // permite cookies e headers de auth
maxAge: 86400, // cacheia preflight por 24 horas
})
);
6. Validação e sanitização de entrada
Toda entrada de dados deve ser validada no servidor — nunca confie na validação do front-end. O front-end pode ser bypassado facilmente com ferramentas como o Postman.
// npm install zod
const { z } = require('zod');
// Define o schema com todas as regras de validação
const schemaCriarUsuario = z.object({
nome: z
.string({ required_error: 'Nome é obrigatório.' })
.min(2, 'Nome deve ter pelo menos 2 caracteres.')
.max(100, 'Nome não pode exceder 100 caracteres.')
.trim(),
email: z
.string({ required_error: 'Email é obrigatório.' })
.email('Email inválido.')
.toLowerCase()
.trim(),
senha: z
.string({ required_error: 'Senha é obrigatória.' })
.min(8, 'Senha deve ter pelo menos 8 caracteres.')
// Exige pelo menos uma letra maiúscula, um número e um caractere especial
.regex(
/^(?=.*[A-Z])(?=.*d)(?=.*[@$!%*?&])/,
'Senha deve conter maiúscula, número e caractere especial.'
),
idade: z
.number()
.int('Idade deve ser um número inteiro.')
.min(0, 'Idade inválida.')
.max(150, 'Idade inválida.')
.optional(),
});
// Middleware de validação reutilizável
// Recebe um schema Zod e retorna um middleware do Express
function validar(schema) {
return (req, res, next) => {
try {
// parse() lança erro se os dados não passarem na validação
// Também transforma os dados (trim, toLowerCase, etc.)
req.body = schema.parse(req.body);
next();
} catch (erro) {
if (erro instanceof z.ZodError) {
// Formata os erros de forma legível
const detalhes = erro.errors.map((e) => `${e.path.join('.')}: ${e.message}`);
return res.status(422).json({
erro: 'Dados inválidos.',
detalhes,
});
}
next(erro);
}
};
}
// Usando nas rotas
app.post('/auth/registrar', validar(schemaCriarUsuario), async (req, res) => {
// req.body agora tem tipos corretos e dados sanitizados
const { nome, email, senha } = req.body;
// ...
});
7. Proteção de rotas sensíveis e dados
Algumas proteções simples que fazem grande diferença no dia a dia.
// ── NUNCA EXPONHA DADOS SENSÍVEIS ────────────────────
// ❌ Retorna a senha (mesmo que em hash) e dados internos
app.get('/usuarios/:id', async (req, res) => {
const usuario = await Usuario.findById(req.params.id);
res.json(usuario); // inclui senha, __v, datas internas, etc.
});
// ✅ Seleciona apenas os campos necessários
app.get('/usuarios/:id', autenticar, async (req, res) => {
const usuario = await Usuario.findById(
req.params.id,
// Projeção — retorna apenas estes campos
'nome email papel criadoEm'
// Senha nunca é retornada (select: false no schema + não listada aqui)
);
res.json(usuario);
});
// ── GARANTA QUE USUÁRIOS SÓ ACESSAM SEUS PRÓPRIOS DADOS ──
// ❌ Qualquer usuário logado pode ver/editar tarefas de qualquer outro
app.get('/tarefas/:id', autenticar, async (req, res) => {
const tarefa = await Tarefa.findById(req.params.id);
res.json(tarefa);
});
// ✅ Filtra sempre pelo usuário autenticado
app.get('/tarefas/:id', autenticar, async (req, res) => {
const tarefa = await Tarefa.findOne({
_id: req.params.id,
usuario: req.usuario._id, // garante que é dono da tarefa
});
if (!tarefa) {
// Retorna 404 (não 403) — não confirmamos que o recurso existe
return res.status(404).json({ erro: 'Tarefa não encontrada.' });
}
res.json(tarefa);
});
// ── VARIÁVEIS DE AMBIENTE NUNCA EXPOSTAS ─────────────
// ❌ Expõe configurações do servidor
app.get('/debug', (req, res) => {
res.json(process.env); // expõe JWT_SECRET, MONGODB_URL, etc.
});
// ✅ Health check expõe apenas o necessário
app.get('/health', (req, res) => {
res.json({
status: 'ok',
ambiente: process.env.NODE_ENV,
uptime: Math.floor(process.uptime()) + 's',
// Nunca inclua process.env aqui
});
});
8. Headers de segurança com Helmet
O Helmet configura uma série de headers HTTP que protegem contra diferentes tipos de ataque. É uma das adições mais simples e eficazes que você pode fazer.
const helmet = require('helmet');
// Aplica todos os headers de segurança padrão
app.use(helmet());
// Os headers que o Helmet configura e o que cada um faz:
//
// Content-Security-Policy
// → Define quais recursos (scripts, estilos, imagens) podem ser carregados
// → Principal defesa contra XSS
//
// X-Content-Type-Options: nosniff
// → Impede que o navegador "adivinhe" o tipo de conteúdo
// → Evita que um script disfarçado de imagem execute
//
// X-Frame-Options: SAMEORIGIN
// → Impede que sua página seja carregada em um iframe de outro domínio
// → Protege contra clickjacking
//
// Strict-Transport-Security (HSTS)
// → Força HTTPS — o navegador nunca usará HTTP novamente para este domínio
// → Protege contra downgrade attacks
//
// X-XSS-Protection
// → Ativa o filtro XSS nativo de browsers mais antigos
//
// Referrer-Policy: no-referrer
// → Controla quanta informação de URL é enviada em requisições cross-origin
9. Proteção contra CSRF
CSRF (Cross-Site Request Forgery) faz com que um usuário autenticado execute ações indesejadas sem saber. Como usamos JWT em headers (não cookies), estamos naturalmente protegidos — cookies são enviados automaticamente, headers não.
// Com JWT em Authorization header: imune a CSRF por design
// O atacante não consegue definir headers customizados em requisições cross-origin
// Se você usar cookies para armazenar o JWT, precisa de proteção CSRF:
// npm install csurf (ou use o padrão Double Submit Cookie)
// Configurando cookies de forma segura (se necessário)
res.cookie('token', jwtToken, {
httpOnly: true, // inacessível via JavaScript (protege contra XSS)
secure: true, // apenas HTTPS
sameSite: 'strict', // não envia em requisições cross-origin (protege contra CSRF)
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 dias em milissegundos
});
10. Dependências seguras
Uma aplicação Node.js típica tem centenas de dependências. Qualquer uma delas pode ter uma vulnerabilidade conhecida.
# Verificar vulnerabilidades nas dependências
npm audit
# Saída típica:
# found 3 vulnerabilities (1 low, 1 moderate, 1 high)
# run `npm audit fix` to fix them
# Corrigir automaticamente (quando possível)
npm audit fix
# Para atualizações que quebram a API (major versions):
npm audit fix --force # use com cuidado — pode quebrar o projeto
# Verificar dependências desatualizadas
npm outdated
# Atualizar uma dependência específica
npm update express
# Remover dependências não usadas
npx depcheck
// package.json — configurando auditoria no CI/CD
{
"scripts": {
"audit": "npm audit --audit-level=high",
"quality": "npm run format:check && npm run lint && npm run audit && npm test"
}
}
# .github/workflows/seguranca.yml
# Roda semanalmente para verificar novas vulnerabilidades
name: Auditoria de Segurança
on:
schedule:
# Todo domingo às 9h
- cron: '0 9 * * 0'
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
# Falha se houver vulnerabilidades de severidade alta ou crítica
- run: npm audit --audit-level=high
Checklist de segurança
Assim como o checklist de deploy, este deve ser revisado antes de qualquer lançamento.
Autenticação e Senhas
─────────────────────────────────────────────────────────
[ ] Senhas armazenadas com bcrypt (fator >= 12)
[ ] JWT com chave forte (>= 256 bits), expiração curta
[ ] Rate limiting em /login, /registrar, /esqueci-senha
[ ] Mesma mensagem de erro para "usuário não existe" e "senha errada"
[ ] Tokens de refresh com chave separada e revogação implementada
Dados e Validação
─────────────────────────────────────────────────────────
[ ] Toda entrada validada no servidor (não só no front-end)
[ ] Tipos verificados antes de queries ao banco
[ ] Usuários só acessam seus próprios dados
[ ] Campos sensíveis com select: false no schema
[ ] Resposta nunca inclui senha, tokens ou dados internos
Headers e Transporte
─────────────────────────────────────────────────────────
[ ] Helmet instalado e configurado
[ ] CORS restrito às origens conhecidas
[ ] HTTPS em produção (Vercel e Railway fazem isso automaticamente)
[ ] Cookies com httpOnly, secure, sameSite se utilizados
Infraestrutura
─────────────────────────────────────────────────────────
[ ] Variáveis de ambiente em .gitignore
[ ] .env.example documentado para o time
[ ] npm audit sem vulnerabilidades altas ou críticas
[ ] Dependências atualizadas regularmente
[ ] Logs não expõem dados sensíveis (senhas, tokens, CPF)
Tarefa para você
Aplique as proteções aprendidas na API do Módulo 4:
// 1. Instale e configure o Helmet
// Verifique os headers de segurança com:
// curl -I https://sua-api.railway.app/health
// 2. Adicione rate limiting com express-rate-limit:
// - 100 req/15min para rotas gerais
// - 5 req/15min para /auth/login e /auth/registrar
// 3. Substitua todas as validações manuais por schemas Zod:
// - schemaCriarUsuario
// - schemaLogin
// - schemaCriarProduto
// - schemaAtualizarProduto
// - schemaCriarTarefa
// 4. Verifique que todas as rotas de tarefas filtram pelo req.usuario._id
// Teste com curl tentando acessar tarefa de outro usuário:
// curl -H "Authorization: Bearer TOKEN_USUARIO_A"
// https://api/tarefas/ID_DA_TAREFA_DO_USUARIO_B
// Deve retornar 404, nunca 403 (não confirmamos a existência)
// 5. Configure CORS em produção restrito apenas ao domínio do front-end
// 6. Rode npm audit e corrija todas as vulnerabilidades altas
// 7. Verifique o .gitignore — rode este comando e confirme que .env
// não aparece na lista de arquivos rastreados:
// git ls-files | grep -E ".env"
// Se aparecer, remova com: git rm --cached .env
Conclusão
Neste artigo você aprendeu:
- Como ataques de injeção funcionam e como se defender com validação de tipos e parâmetros
- XSS — o ataque mais comum, como o React protege por padrão e quando usar DOMPurify
- Autenticação segura — bcrypt com fator adequado, JWT com expiração, proteção contra timing attacks
- Rate limiting para proteger contra força bruta em rotas de autenticação
- CORS configurado corretamente — permitindo apenas origens conhecidas
- Validação de entrada com Zod — uma camada de proteção robusta e reutilizável
- Controle de acesso — garantindo que usuários só acessam seus próprios dados
- Headers de segurança com Helmet e o que cada um protege
- Por que JWT em headers protege contra CSRF por design
- Como auditar e manter dependências seguras
- O checklist de segurança para revisar antes de cada lançamento
No próximo artigo vamos aprender sobre performance em aplicações web — como medir, identificar gargalos e otimizar tanto o front-end quanto o back-end.
📚 Fontes e Referências
- OWASP Top 10: https://owasp.org/www-project-top-ten
- OWASP Cheat Sheet — Authentication: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- OWASP Cheat Sheet — JWT: https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html
- Helmet.js: https://helmetjs.github.io
- express-rate-limit: https://github.com/express-rate-limit/express-rate-limit
- Zod — Documentação: https://zod.dev
- DOMPurify: https://github.com/cure53/DOMPurify
- bcrypt — Por que fator 12: https://security.stackexchange.com/questions/17207
- npm audit: https://docs.npmjs.com/cli/v10/commands/npm-audit
- The Web Application Hacker's Handbook — Stuttard e Pinto (Wiley)
- Node.js Security — Liran Tal (Leanpub)