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
| Ranking | Categoria | Descricao |
|---|---|---|
| A01 | Falha de Controle de Acesso | Ausencia ou falha na verificacao de autorizacao |
| A02 | Falha de Criptografia | Protecao inadequada de dados sensiveis |
| A03 | Injecao | SQL, XSS, Command Injection |
| A04 | Design Inseguro | Design sem considerar seguranca |
| A05 | Configuracao de Seguranca Incorreta | Configuracoes padrao, funcionalidades desnecessarias |
| A06 | Componentes Vulneraveis | Bibliotecas com vulnerabilidades conhecidas |
| A07 | Falha de Autenticacao | Falhas no mecanismo de autenticacao |
| A08 | Falha de Integridade | Atualizacoes inseguras, CI/CD |
| A09 | Falha de Log e Monitoramento | Falha na deteccao de ataques |
| A10 | SSRF | Server-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),
});
}
SameSite Cookie
// 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
- Validacao de Entrada: Validar e sanitizar todas as entradas do usuario
- Autenticacao/Autorizacao: Implementar controle de acesso adequado
- Criptografia: Criptografar dados sensiveis, forcar HTTPS
- Gerenciamento de Sessao: Configuracao segura de Cookie
- Dependencias: Verificacao regular de vulnerabilidades
Checklist de Operacao
- Log/Monitoramento: Mecanismo de deteccao de anomalias
- Resposta a Incidentes: Elaboracao de plano de resposta
- Auditorias Regulares: Testes de penetracao
- 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.