Melhores Praticas de Seguranca Web - Contramedidas OWASP Top 10

20 min leitura | 2025.12.02

A seguranca de aplicacoes web e um elemento importante que os desenvolvedores devem considerar desde o inicio. O Top 10 publicado pela OWASP (Open Web Application Security Project) e uma lista das vulnerabilidades mais comuns e perigosas, e todo desenvolvedor deveria entende-lo. Este artigo explica cada vulnerabilidade do OWASP Top 10 e suas contramedidas praticas.

OWASP Top 10 2021

Lista de Vulnerabilidades

RankingCategoriaDescricao
A01Falha de Controle de AcessoAusencia ou falha na verificacao de autorizacao
A02Falha de CriptografiaProtecao inadequada de dados sensiveis
A03InjecaoSQL, XSS, Command Injection
A04Design InseguroDesign sem considerar seguranca
A05Configuracao de Seguranca IncorretaConfiguracoes padrao, funcionalidades desnecessarias
A06Componentes VulneraveisBibliotecas com vulnerabilidades conhecidas
A07Falha de AutenticacaoFalhas no mecanismo de autenticacao
A08Falha de IntegridadeAtualizacoes inseguras, CI/CD
A09Falha de Log e MonitoramentoFalha na deteccao de ataques
A10SSRFServer-Side Request Forgery

A03: Ataques de Injecao

SQL Injection

// Codigo vulneravel
async function getUser(userId: string) {
  const query = `SELECT * FROM users WHERE id = '${userId}'`;
  return db.query(query);
}
// userId = "1' OR '1'='1" → Todos os usuarios sao obtidos

// Codigo seguro (consulta parametrizada)
async function getUser(userId: string) {
  const query = 'SELECT * FROM users WHERE id = $1';
  return db.query(query, [userId]);
}

// Usando ORM (Prisma)
async function getUser(userId: string) {
  return prisma.user.findUnique({
    where: { id: userId }
  });
}

XSS (Cross-Site Scripting)

// Codigo vulneravel
function renderComment(comment: string) {
  document.getElementById('comments').innerHTML = comment;
}
// comment = "<script>alert('XSS')</script>" → Script executado

// Codigo seguro (escape)
function escapeHtml(text: string): string {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

function renderComment(comment: string) {
  const escaped = escapeHtml(comment);
  document.getElementById('comments').innerHTML = escaped;
}

// Mais seguro: usar textContent
function renderComment(comment: string) {
  document.getElementById('comments').textContent = comment;
}

// React/Vue fazem escape automaticamente
function CommentList({ comments }: { comments: string[] }) {
  return (
    <ul>
      {comments.map((c, i) => <li key={i}>{c}</li>)}
    </ul>
  );
}

Content Security Policy (CSP)

// Configuracao CSP no Express.js
import helmet from 'helmet';

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'strict-dynamic'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.example.com"],
    fontSrc: ["'self'", "https://fonts.gstatic.com"],
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: [],
  },
}));

A01: Falha de Controle de Acesso

Implementacao de Verificacao de Autorizacao

// Codigo vulneravel
app.get('/api/users/:id', async (req, res) => {
  const user = await db.user.findUnique({ where: { id: req.params.id } });
  res.json(user);  // Qualquer pessoa pode acessar
});

// Codigo seguro
app.get('/api/users/:id', authenticate, async (req, res) => {
  const userId = req.params.id;
  const currentUser = req.user;

  // Apenas o proprio usuario ou administrador pode acessar
  if (userId !== currentUser.id && currentUser.role !== 'admin') {
    return res.status(403).json({ error: 'Forbidden' });
  }

  const user = await db.user.findUnique({ where: { id: userId } });
  res.json(user);
});

Contramedidas para IDOR (Insecure Direct Object Reference)

// Codigo vulneravel
app.get('/api/orders/:orderId', async (req, res) => {
  const order = await db.order.findUnique({
    where: { id: req.params.orderId }
  });
  res.json(order);  // Pode ver pedidos de outras pessoas
});

// Codigo seguro
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.order.findFirst({
    where: {
      id: req.params.orderId,
      userId: req.user.id  // Verificacao de proprietario
    }
  });

  if (!order) {
    return res.status(404).json({ error: 'Order not found' });
  }

  res.json(order);
});

RBAC (Controle de Acesso Baseado em Funcoes)

// Definicao de funcoes
const ROLES = {
  admin: ['read', 'write', 'delete', 'manage_users'],
  editor: ['read', 'write'],
  viewer: ['read'],
} as const;

// Middleware de autorizacao
function authorize(...requiredPermissions: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = req.user?.role;
    const userPermissions = ROLES[userRole] || [];

    const hasPermission = requiredPermissions.every(
      p => userPermissions.includes(p)
    );

    if (!hasPermission) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }

    next();
  };
}

// Exemplo de uso
app.delete('/api/posts/:id',
  authenticate,
  authorize('delete'),
  deletePost
);

A07: Falha de Autenticacao

Processamento Seguro de Senhas

import bcrypt from 'bcrypt';
import crypto from 'crypto';

// Hash de senha
async function hashPassword(password: string): Promise<string> {
  const saltRounds = 12;
  return bcrypt.hash(password, saltRounds);
}

// Verificacao de senha
async function verifyPassword(password: string, hash: string): Promise<boolean> {
  return bcrypt.compare(password, hash);
}

// Validacao de forca da senha
function validatePasswordStrength(password: string): { valid: boolean; errors: string[] } {
  const errors: string[] = [];

  if (password.length < 12) {
    errors.push('A senha deve ter pelo menos 12 caracteres');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Deve incluir letras maiusculas');
  }
  if (!/[a-z]/.test(password)) {
    errors.push('Deve incluir letras minusculas');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Deve incluir numeros');
  }
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('Deve incluir caracteres especiais');
  }

  return { valid: errors.length === 0, errors };
}

Contramedidas para Forca Bruta

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Limitacao de taxa (endpoint de login)
const loginLimiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
    prefix: 'rl:login:'
  }),
  windowMs: 15 * 60 * 1000,  // 15 minutos
  max: 5,  // Maximo 5 vezes
  skipSuccessfulRequests: true,  // Nao conta requisicoes bem-sucedidas
  message: {
    error: 'Muitas tentativas de login. Por favor, tente novamente em 15 minutos.'
  }
});

app.post('/api/auth/login', loginLimiter, loginHandler);

// Funcionalidade de bloqueio de conta
async function checkAccountLock(userId: string): Promise<boolean> {
  const lockKey = `lock:${userId}`;
  const failKey = `fail:${userId}`;

  const lockUntil = await redis.get(lockKey);
  if (lockUntil && Date.now() < parseInt(lockUntil)) {
    return true;  // Bloqueado
  }

  return false;
}

async function recordFailedAttempt(userId: string): Promise<void> {
  const failKey = `fail:${userId}`;
  const lockKey = `lock:${userId}`;

  const attempts = await redis.incr(failKey);
  await redis.expire(failKey, 3600);  // Reset em 1 hora

  if (attempts >= 5) {
    // Bloqueia por 30 minutos
    await redis.set(lockKey, Date.now() + 30 * 60 * 1000);
    await redis.expire(lockKey, 1800);
  }
}

Gerenciamento de Sessao

import session from 'express-session';
import RedisStore from 'connect-redis';

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET!,
  name: 'sessionId',  // Alterar o padrao 'connect.sid'
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // Inacessivel via JavaScript
    secure: true,       // Requer HTTPS
    sameSite: 'lax',    // Protecao CSRF
    maxAge: 24 * 60 * 60 * 1000,  // 24 horas
  }
}));

// Invalidacao de sessao no logout
app.post('/api/auth/logout', (req, res) => {
  const sessionId = req.sessionID;

  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Falha ao fazer logout' });
    }

    // Tambem excluir o Cookie
    res.clearCookie('sessionId');
    res.json({ message: 'Logout realizado com sucesso' });
  });
});

Contramedidas para CSRF (Cross-Site Request Forgery)

Token CSRF

import csrf from 'csurf';

// Middleware CSRF
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  }
});

// Endpoint para obter token
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Endpoint protegido
app.post('/api/transfer', csrfProtection, (req, res) => {
  // Token CSRF e verificado automaticamente
  // ...
});
// Uso no frontend
async function makeRequest(url: string, data: object) {
  // Obter token CSRF
  const { csrfToken } = await fetch('/api/csrf-token').then(r => r.json());

  return fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken,
    },
    credentials: 'include',
    body: JSON.stringify(data),
  });
}
// Protecao CSRF moderna
res.cookie('session', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',  // ou 'lax'
});

A02: Falha de Criptografia

Criptografia de Dados

import crypto from 'crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');  // 32 bytes

// Criptografia
function encrypt(plaintext: string): string {
  const iv = crypto.randomBytes(12);  // GCM usa IV de 12 bytes
  const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv);

  let encrypted = cipher.update(plaintext, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  // Combinar IV + AuthTag + texto cifrado
  return iv.toString('hex') + authTag.toString('hex') + encrypted;
}

// Descriptografia
function decrypt(ciphertext: string): string {
  const iv = Buffer.from(ciphertext.slice(0, 24), 'hex');
  const authTag = Buffer.from(ciphertext.slice(24, 56), 'hex');
  const encrypted = ciphertext.slice(56);

  const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv);
  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

Forcando HTTPS

// Cabecalho HSTS
app.use(helmet.hsts({
  maxAge: 31536000,  // 1 ano
  includeSubDomains: true,
  preload: true,
}));

// Redirecionamento de HTTP para HTTPS
app.use((req, res, next) => {
  if (req.headers['x-forwarded-proto'] !== 'https') {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

A10: SSRF (Server-Side Request Forgery)

Contramedidas para SSRF

import { URL } from 'url';
import dns from 'dns/promises';

// Lista de hosts permitidos
const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];

async function fetchUrl(urlString: string): Promise<Response> {
  const url = new URL(urlString);

  // Verificacao de protocolo
  if (!['http:', 'https:'].includes(url.protocol)) {
    throw new Error('Invalid protocol');
  }

  // Verificacao de hostname
  if (!ALLOWED_HOSTS.includes(url.hostname)) {
    throw new Error('Host not allowed');
  }

  // Prevenir acesso a IPs privados
  const addresses = await dns.resolve4(url.hostname);
  for (const addr of addresses) {
    if (isPrivateIP(addr)) {
      throw new Error('Private IP not allowed');
    }
  }

  return fetch(url.toString());
}

function isPrivateIP(ip: string): boolean {
  const parts = ip.split('.').map(Number);

  // Localhost
  if (parts[0] === 127) return true;

  // Endereco privado
  if (parts[0] === 10) return true;
  if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
  if (parts[0] === 192 && parts[1] === 168) return true;

  // Link-local
  if (parts[0] === 169 && parts[1] === 254) return true;

  return false;
}

Cabecalhos de Seguranca

Configuracao de Cabecalhos Recomendada

import helmet from 'helmet';

app.use(helmet());

// Configuracao individual
app.use(helmet.frameguard({ action: 'deny' }));  // Protecao contra clickjacking
app.use(helmet.noSniff());  // Protecao contra MIME type sniffing
app.use(helmet.xssFilter());  // Filtro XSS
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));

// Cabecalho customizado
app.use((req, res, next) => {
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
  next();
});

Checklist de Cabecalhos de Resposta

Checklist de Cabecalhos de Seguranca:

□ Content-Security-Policy
□ Strict-Transport-Security
□ X-Frame-Options
□ X-Content-Type-Options
□ Referrer-Policy
□ Permissions-Policy
□ X-XSS-Protection (legado)

Seguranca de Dependencias

Verificacao de Vulnerabilidades

# npm audit
npm audit
npm audit fix

# Snyk
npx snyk test
npx snyk monitor

# GitHub Dependabot
# Configurar em .github/dependabot.yml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Resumo

A seguranca web requer esforco continuo.

Checklist de Desenvolvimento

  1. Validacao de Entrada: Validar e sanitizar todas as entradas do usuario
  2. Autenticacao/Autorizacao: Implementar controle de acesso adequado
  3. Criptografia: Criptografar dados sensiveis, forcar HTTPS
  4. Gerenciamento de Sessao: Configuracao segura de Cookie
  5. Dependencias: Verificacao regular de vulnerabilidades

Checklist de Operacao

  1. Log/Monitoramento: Mecanismo de deteccao de anomalias
  2. Resposta a Incidentes: Elaboracao de plano de resposta
  3. Auditorias Regulares: Testes de penetracao
  4. Educacao: Conscientizacao de seguranca dos desenvolvedores

Seguranca nao e uma “funcionalidade adicionada depois”, mas um elemento que deve ser incorporado desde a fase de design.

← Voltar para a lista