Javascript

Segurança em aplicações web Já leu

20 min de leitura

Segurança em aplicações web
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 co

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)
Comentários

Mais em Javascript

Dominando o JavaScript
Dominando o JavaScript

Estou estudando Javascript a um longo tempo. N&atilde;o sei precisar quanto t...

Projeto Final: Revisão Completa e Aplicação de Produção
Projeto Final: Revisão Completa e Aplicação de Produção

Artigo 52 — Projeto Final: Revisão Completa e Aplicação de ProduçãoPor Prof....

Promises: resolvendo o Callback Hell
Promises: resolvendo o Callback Hell

No artigo anterior vimos o Callback Hell &mdash; c&oacute;digo que cresce hor...