Introdução ao OWASP Top 10
O OWASP Top 10 é uma lista de referência internacional que documenta as dez vulnerabilidades de segurança mais críticas encontradas em aplicações web. Publicado pela Open Web Application Security Project, este documento é atualizado regularmente — a versão mais recente é de 2021 — e serve como base para desenvolvimento seguro, testes de penetração e auditorias de segurança. Ignorar essas vulnerabilidades não é apenas uma questão de compliance, mas de responsabilidade com seus usuários e dados.
A maioria das brechas de segurança não acontece porque o atacante descobriu um zero-day, mas porque vulnerabilidades conhecidas e documentadas não foram mitigadas. O OWASP Top 10 existe justamente para evitar que você cometa os mesmos erros que milhares de desenvolvedores cometem todos os dias. Neste artigo, abordaremos cada uma dessas dez categorias com exemplos práticos e soluções reais que você pode implementar imediatamente em seus projetos.
1. Injection (Injeção)
Injeção ocorre quando dados não confiáveis são enviados para um interpretador como parte de um comando ou query. O atacante injeta código malicioso que é executado no servidor, permitindo acesso não autorizado, modificação de dados ou até compromisso total do sistema. SQL Injection é a forma mais comum, mas você também encontrará injeção em LDAP, OS commands, XML e outras linguagens.
SQL Injection
SQL Injection acontece quando entrada do usuário é concatenada diretamente em uma query SQL sem validação ou sanitização. Veja um exemplo vulnerável:
# VULNERÁVEL - NÃO FAÇA ISTO
import sqlite3
def login_user(username, password):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# Concatenação direta - permite injeção
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
result = cursor.fetchone()
conn.close()
return result is not None
Se o usuário inserir admin' -- como username, a query se torna:
SELECT * FROM users WHERE username = 'admin' --' AND password = '...'
O -- comenta o resto da query, ignorando a senha completamente.
A solução é usar prepared statements (consultas parametrizadas):
# SEGURO - Use prepared statements
import sqlite3
def login_user(username, password):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# Placeholders (?) segregam dados do código SQL
query = "SELECT * FROM users WHERE username = ? AND password = ?"
cursor.execute(query, (username, password))
result = cursor.fetchone()
conn.close()
return result is not None
Prepared statements garantem que o input do usuário seja tratado como dado, nunca como código executável. Este é o padrão ouro e funciona em todas as linguagens e databases.
Command Injection
Command injection é similiar, mas ocorre quando você passa input de usuário diretamente para comandos do sistema operacional:
# VULNERÁVEL - NÃO FAÇA ISTO
import os
def generate_report(filename):
# Se filename for "report.pdf; rm -rf /", você tem um problema sério
os.system(f"generate_pdf {filename}")
A solução é usar subprocess com uma lista de argumentos e shell=False:
# SEGURO
import subprocess
def generate_report(filename):
# Lista de argumentos é mais segura; shell=False impede interpretação de shell metacharacters
subprocess.run(['generate_pdf', filename], shell=False, check=True)
2. Broken Authentication (Autenticação Quebrada)
Autenticação quebrada permite que atacantes comprometam senhas, tokens de sessão ou implementem ataques de força bruta. Isso inclui falhas em gerenciamento de sessão, senhas fracas, recovery flows inadequados e falta de proteção em endpoints sensíveis.
Uma das falhas mais comuns é armazenar senhas em plain text ou com hash fraco. Senhas devem ser hasheadas com algoritmos modernos como bcrypt, scrypt ou PBKDF2 com salt:
# VULNERÁVEL - armazena em plain text
def register_user(username, password):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)",
(username, password)) # NUNCA FAÇA ISTO
conn.commit()
conn.close()
# SEGURO - usa bcrypt
import bcrypt
def register_user(username, password):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# bcrypt gera um salt aleatório e hash seguro automaticamente
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)",
(username, hashed_password))
conn.commit()
conn.close()
def login_user(username, password):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT password FROM users WHERE username = ?", (username,))
result = cursor.fetchone()
conn.close()
if result:
# bcrypt.checkpw compara senhas com segurança
return bcrypt.checkpw(password.encode('utf-8'), result[0])
return False
Além disso, implemente proteção contra força bruta com rate limiting e bloqueio temporário de contas após múltiplas tentativas de login falhadas. Use session tokens seguros (gerados com secrets em Python), defina expiração apropriada e considere implementar autenticação multifator (MFA) para contas sensíveis.
3. Sensitive Data Exposure (Exposição de Dados Sensíveis)
Dados sensíveis — senhas, tokens, informações financeiras, dados de saúde — precisam ser protegidos tanto em trânsito quanto em repouso. Isso inclui usar HTTPS/TLS, criptografia de dados armazenados e implementar controles de acesso apropriados.
Proteção em Trânsito
Sempre use HTTPS em produção. HTTP trafega em plain text e qualquer um na rede (especialmente em WiFi público) pode interceptar seus dados:
# Flask - força HTTPS em produção
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
Talisman(app, force_https=True) # Redireciona HTTP para HTTPS automaticamente
@app.route('/api/user')
def get_user():
return {'user': 'data'} # Trafega encriptado via HTTPS
Proteção em Repouso
Dados sensíveis armazenados devem ser criptografados:
# Criptografia de dados sensíveis
from cryptography.fernet import Fernet
import os
# Gere uma chave uma vez e armazene-a com segurança (variável de ambiente, key management service, etc)
encryption_key = os.getenv('ENCRYPTION_KEY', Fernet.generate_key())
cipher = Fernet(encryption_key)
def store_credit_card(card_number):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
# Criptografe antes de armazenar
encrypted_card = cipher.encrypt(card_number.encode())
cursor.execute("INSERT INTO payments (card) VALUES (?)", (encrypted_card,))
conn.commit()
conn.close()
def retrieve_credit_card(card_id):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT card FROM payments WHERE id = ?", (card_id,))
encrypted_card = cursor.fetchone()[0]
conn.close()
# Descriptografe quando necessário
return cipher.decrypt(encrypted_card).decode()
Nunca log dados sensíveis. Não armazene mais dados do que necessário. Implemente controle de acesso para que apenas usuários autorizados vejam dados sensíveis. Considere tokenização para dados como números de cartão — armazene um token em vez do número real.
4. XML External Entity (XXE) e Entity Parsing Attacks
XXE ocorre quando um parser XML processa entidades externas não confiáveis. Um atacante pode ler arquivos do servidor, fazer requisições para serviços internos ou provocar negação de serviço. Muitas aplicações ainda processam XML sem desabilitar features perigosas.
Veja um exemplo vulnerável:
# VULNERÁVEL - processa entidades externas
import xml.etree.ElementTree as ET
def parse_user_xml(xml_string):
# O parser padrão permite entidades externas
root = ET.fromstring(xml_string)
return root.tag
Se alguém enviar este XML malicioso:
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<user>
<name>&xxe;</name>
</user>
O parser vai tentar ler /etc/passwd do servidor.
A solução é desabilitar explicitamente entidades externas:
# SEGURO - desabilita XXE
import xml.etree.ElementTree as ET
def parse_user_xml(xml_string):
# Desabilita DOCTYPE, entidades externas e DTD
parser = ET.XMLParser()
parser.parser.EntityParser = None
# Melhor ainda: use defusedxml
from defusedxml import ElementTree as DefusedET
root = DefusedET.fromstring(xml_string)
return root.tag
A biblioteca defusedxml é especializada em mitigar vulnerabilidades XML. Use-a sempre que processar XML de fontes não confiáveis:
# Recomendado
from defusedxml.ElementTree import parse as safe_parse
def parse_xml_file(filepath):
tree = safe_parse(filepath)
return tree.getroot()
5. Broken Access Control (Controle de Acesso Quebrado)
Controle de acesso quebrado significa que usuários conseguem acessar recursos ou executar ações para as quais não têm permissão. Isso inclui acesso a dados de outros usuários, escalação de privilégios, manipulação de URLs/parâmetros para acessar conteúdo protegido.
Um erro comum é confiar apenas em verificações no frontend ou em ofuscação de IDs:
# VULNERÁVEL - não valida permissões no backend
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/users/<user_id>/profile')
def get_user_profile(user_id):
# Qualquer pessoa pode acessar qualquer perfil
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
conn.close()
return jsonify(user)
A solução é sempre validar permissões no servidor:
# SEGURO - valida permissões
from flask import Flask, request, jsonify, session
app = Flask(__name__)
def get_current_user():
# Obtém usuário autenticado da sessão
return session.get('user_id')
@app.route('/api/users/<user_id>/profile')
def get_user_profile(user_id):
current_user_id = get_current_user()
if not current_user_id:
return jsonify({'error': 'Não autenticado'}), 401
# Valida que o usuário só pode acessar seu próprio perfil
if int(user_id) != current_user_id:
return jsonify({'error': 'Acesso negado'}), 403
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
user = cursor.fetchone()
conn.close()
return jsonify(user)
Implemente role-based access control (RBAC) onde necessário:
# RBAC - controle baseado em papéis
def require_role(*allowed_roles):
def decorator(f):
def wrapper(*args, **kwargs):
current_user = get_current_user()
user_role = get_user_role(current_user)
if user_role not in allowed_roles:
return jsonify({'error': 'Acesso negado'}), 403
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
return decorator
@app.route('/api/admin/users')
@require_role('admin')
def list_all_users():
# Apenas administradores podem acessar
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
users = cursor.fetchall()
conn.close()
return jsonify(users)
Sempre valide permissões no servidor. Não confie em tokens de sessão ou IDs que vêm do cliente. Implemente principle of least privilege — dê aos usuários apenas as permissões mínimas necessárias.
6. Security Misconfiguration (Configuração Segurança Inadequada)
Misconfigurações incluem defaults inseguros, permissões inadequadas, desativação de recursos de segurança, falta de patches, headers de segurança ausentes e exposição de informações sensíveis em mensagens de erro.
Headers de Segurança
Muitos servidores não enviam headers HTTP essenciais de segurança. Adicione-os:
# Flask - adiciona headers de segurança
from flask import Flask
from flask_talisman import Talisman
app = Flask(__name__)
# Talisman configurado corretamente
Talisman(app,
force_https=True,
strict_transport_security=True,
strict_transport_security_max_age=31536000, # 1 ano
content_security_policy={
'default-src': "'self'",
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'"],
}
)
@app.route('/')
def index():
return 'Seguro'
Os headers mais importantes:
- Strict-Transport-Security: força HTTPS
- Content-Security-Policy: previne XSS e injection
- X-Content-Type-Options: previne MIME sniffing
- X-Frame-Options: previne clickjacking
Mensagens de Erro Genéricas
Não exponha detalhes técnicos em mensagens de erro:
# VULNERÁVEL - expõe stack trace
@app.route('/api/data')
def get_data():
try:
result = 1 / 0 # Simula erro
except Exception as e:
return jsonify({'error': str(e)}), 500 # Expõe detalhes
# SEGURO - mensagem genérica
@app.route('/api/data')
def get_data():
try:
result = 1 / 0
except Exception as e:
# Log o erro internamente
app.logger.error(f"Erro: {str(e)}", exc_info=True)
# Retorna mensagem genérica
return jsonify({'error': 'Erro interno do servidor'}), 500
Desabilite directory listing, remova softwares desnecessários, aplique patches regularmente, mude defaults (senhas, portas, configurações), desabilite debug em produção e implemente logging de eventos de segurança.
7. Cross-Site Scripting (XSS)
XSS ocorre quando a aplicação inclui dados não confiáveis em páginas web sem validação ou escape apropriados. O atacante injeta código JavaScript que executa no navegador da vítima, roubando sessões, credentials ou malware.
Existem três tipos: Stored (armazenado no banco), Reflected (na URL/resposta) e DOM-based (manipulação de DOM via JavaScript).
XSS Reflected
# VULNERÁVEL - renderiza entrada sem escape
from flask import Flask, request
app = Flask(__name__)
@app.route('/search')
def search():
query = request.args.get('q', '')
# Se q for "<script>alert('XSS')</script>", ele executará
return f"<h1>Resultados para: {query}</h1>"
Se alguém visitar /search?q=<script>alert('roubando cookies')</script>, o script executará.
A solução é fazer escape de HTML:
# SEGURO - faz escape de HTML
from flask import Flask, request, escape
app = Flask(__name__)
@app.route('/search')
def search():
query = request.args.get('q', '')
# escape converte < para <, > para >, etc
safe_query = escape(query)
return f"<h1>Resultados para: {safe_query}</h1>"
Em templates Jinja2, use {{ variable }} em vez de {{ variable|safe }}:
<!-- VULNERÁVEL -->
<h1>{{ user_input|safe }}</h1>
<!-- SEGURO - Jinja2 faz escape automaticamente -->
<h1>{{ user_input }}</h1>
XSS Stored
# VULNERÁVEL - armazena e renderiza sem escape
@app.route('/comment', methods=['POST'])
def add_comment():
comment = request.form.get('text')
# Armazena o que o usuário escreveu
save_to_db(comment)
return redirect('/page')
@app.route('/page')
def show_page():
comments = get_from_db()
html = ""
for comment in comments:
# Renderiza sem escape - XSS armazenado
html += f"<p>{comment}</p>"
return html
A solução é fazer escape na renderização:
from flask import Flask, escape
# Armazene como está
@app.route('/comment', methods=['POST'])
def add_comment():
comment = request.form.get('text')
save_to_db(comment)
return redirect('/page')
# Faça escape na renderização
@app.route('/page')
def show_page():
comments = get_from_db()
html = ""
for comment in comments:
# escape previne execução
html += f"<p>{escape(comment)}</p>"
return html
Valide e faça escape de todo input do usuário antes de renderizar em HTML. Use Content Security Policy (CSP) para limitar fontes de scripts. Implemente sanitização de HTML se precisar permitir algumas tags (use bibliotecas como bleach).
8. Insecure Deserialization (Desserialização Insegura)
Desserialização é converter dados serializados (como JSON ou pickle) de volta em objetos. Se atacantes podem manipular dados serializados, podem criar objetos maliciosos que executam código no servidor.
Este é um problema especialmente crítico com pickle em Python:
# MUITO VULNERÁVEL - nunca use pickle com dados não confiáveis
import pickle
def load_user_data(serialized_data):
# Se serialized_data for malicioso, pode executar código arbitrário
user = pickle.loads(serialized_data) # NUNCA FAÇA ISTO com dados do usuário
return user
Pickle permite serializar qualquer objeto Python, incluindo funções. Um atacante pode criar um objeto que executa código ao ser desserializado:
# Como um atacante criaria payload malicioso
import pickle
import os
class Exploit:
def __reduce__(self):
# Executa comando ao desserializar
return (os.system, ('rm -rf /',))
malicious_payload = pickle.dumps(Exploit())
# Se essa payload for desserializado, deleta tudo
A solução é usar JSON, que é seguro por natureza:
# SEGURO - use JSON
import json
def load_user_data(json_string):
# JSON não permite execução de código arbitrário
user = json.loads(json_string)
return user
def save_user_data(user):
return json.dumps(user)
Se você absolutamente precisa usar pickle, faça com dados que você controla. Se receber dados serializados de usuários, valide assinatura com HMAC:
# Se deve usar pickle, implemente assinatura
import pickle
import hmac
import hashlib
SECRET_KEY = 'sua-chave-secreta-muito-segura'
def serialize_safely(obj):
serialized = pickle.dumps(obj)
signature = hmac.new(SECRET_KEY.encode(), serialized, hashlib.sha256).digest()
return serialized + signature
def deserialize_safely(data):
serialized = data[:-32] # Últimos 32 bytes são a assinatura
signature = data[-32:]
expected_signature = hmac.new(SECRET_KEY.encode(), serialized, hashlib.sha256).digest()
if not hmac.compare_digest(signature, expected_signature):
raise ValueError("Dados foram modificados!")
return pickle.loads(serialized)
Prefira JSON ou outros formatos seguros. Se deve desserializar, valide assinatura. Nunca desserialize dados de usuários sem verificação. Implemente object type allowlisting se possível.
9. Using Components with Known Vulnerabilities (Componentes com Vulnerabilidades Conhecidas)
Muitos projetos dependem de bibliotecas e frameworks de terceiros. Se essas dependências têm vulnerabilidades conhecidas, sua aplicação herda o risco. Manter dependências atualizadas é essencial.
Ferramentas como pip-audit em Python verificam vulnerabilidades em suas dependências:
# Instale pip-audit
pip install pip-audit
# Verifique vulnerabilidades nas suas dependências
pip-audit
# Output exemplo:
# Found 2 known security vulnerabilities in 2 packages
# Name: django
# Version: 2.1.0
# VULN-ID: GHSA-xxxx
# Fixed Version: 2.1.11
Mantenha um requirements.txt atualizado e revise regularmente:
# requirements.txt - verifique versões periodicamente
Flask==2.3.2
requests==2.31.0
cryptography==41.0.0
bcrypt==4.0.1
Crie um processo de atualização automático:
# Exemplo usando GitHub Dependabot
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
allow:
- dependency-type: "all"
Monitore CVEs de suas dependências regularmente. Use pip-audit, OWASP Dependency-Check ou Snyk. Defina política de atualização: atualize patches de segurança imediatamente, updates regulares para versões menores, planejar major version updates. Considere usar containers com imagens atualizadas (docker pull regularmente).
10. Insufficient Logging and Monitoring (Logging e Monitoramento Insuficientes)
Sem logging adequado, você não consegue detectar ataques, investigar incidentes ou cumprir conformidade regulatória. Sem monitoramento, ataques acontecem silenciosamente.
Implementando Logging Seguro
import logging
from logging.handlers import RotatingFileHandler
import os
# Configure logging estruturado
def setup_logging(app):
if not os.path.exists('logs'):
os.mkdir('logs')
# Registre eventos de segurança
file_handler = RotatingFileHandler('logs/security.log',
maxBytes=10485760, # 10MB
backupCount=10)
formatter = logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [%(pathname)s:%(lineno)d]'
)
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.WARNING)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.WARNING)
# Registre eventos importantes
from flask import Flask, request, session
app = Flask(__name__)
setup_logging(app)
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
if authenticate(username, password):
app.logger.warning(f"Login bem-sucedido para usuário: {username}")
session['user_id'] = get_user_id(username)
return redirect('/')
else:
# Registre tentativa falhada
app.logger.warning(f"Login falhou para usuário: {username} de IP: {request.remote_addr}")
return "Login inválido", 401
@app.route('/api/users', methods=['POST'])
def create_user():
# Registre mudanças sensíveis
user_id = request.json.get('id')
action_user = session.get('user_id')
app.logger.warning(f"Usuário {action_user} criou novo usuário {user_id}")
# ... criar usuário
return {'status': 'criado'}
Monitoramento Proativo
# Alerte sobre comportamento suspeito
import time
from collections import defaultdict
# Rastreie tentativas de login falhadas
failed_logins = defaultdict(list)
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
remote_ip = request.remote_addr
if not authenticate(username, password):
failed_logins[remote_ip].append(time.time())
# Se mais de 5 tentativas em 5 minutos, bloqueie
recent_failures = [t for t in failed_logins[remote_ip]
if time.time() - t < 300]
if len(recent_failures) > 5:
app.logger.critical(f"Possível brute force de IP: {remote_ip}")
return "IP bloqueado por segurança", 429
return "Login inválido", 401
Registre: falhas de autenticação, mudanças de permissões, acesso a dados sensíveis, mudanças de configuração, eventos de segurança. Use timestamps UTC precisos. Não registre senhas, tokens ou dados sensíveis. Centralizar logs em SIEM (Security Information and Event Management) como ELK Stack. Implemente alertas em tempo real para eventos críticos.
Conclusão
O OWASP Top 10 não é um conjunto estático de vulnerabilidades para memorizar, mas um reflexo das ameaças mais prevalentes encontradas em aplicações web reais. Neste artigo, cobri as dez categorias com exemplos funcionais que você pode implementar hoje: Injection é evitada com prepared statements, Broken Authentication com bcrypt e rate limiting, Sensitive Data Exposure com HTTPS e criptografia, XXE com defusedxml, Broken Access Control com validação server-side, Misconfiguration com headers de segurança, XSS com escape de HTML, Deserialization com JSON, Vulnerable Components com monitoramento de dependências e Insufficient Logging com eventos estruturados.
A segunda aprendizagem essencial é que segurança não é um checklist final, mas um processo contínuo. Vulnerabilidades são descobertas regularmente, frameworks são atualizados, novas técnicas de ataque surgem. Mantenha-se informado lendo CVEs, participando de comunidades de segurança e revisando código regularmente. A maioria dos ataques explora vulnerabilidades conhecidas que já foram documentadas e corrigidas — não ser vítima dessas vulnerabilidades é principalmente questão de diligência e implementação adequada.
Finalmente, lembre-se que segurança é responsabilidade de todo desenvolvedor, não apenas do time de segurança. Integre pensamento de segurança no seu ciclo de desenvolvimento desde o início: design seguro antes de codificar, code review focado em segurança, testes de segurança automatizados em CI/CD. Quando você trata segurança como parte integral do desenvolvimento, não como algo "bolado depois", você constrói aplicações significativamente mais robustas.
Referências
- OWASP Top 10 2021 - Documentação oficial da OWASP com descrições detalhadas, exemplos e mitigações
- OWASP Cheat Sheet Series - Guias práticos para implementar segurança em diferentes linguagens e frameworks
- PortSwigger Web Security Academy - Tutoriais interativos sobre cada vulnerabilidade com labs práticos
- SANS Top 25 Most Dangerous Software Weaknesses - Perspectiva complementar das vulnerabilidades mais críticas
- OWASP Testing Guide - Metodologia completa para teste de penetração e validação de segurança