Segurança
Esta página consolida todas as medidas de segurança do Neemias, cobrindo autenticação, criptografia em repouso, segurança de rede, proteção de API, proteção de dados e infraestrutura. O projeto passou por uma auditoria de segurança com 14 findings resolvidos (ver STATE.md para o histórico completo).
Para reportar vulnerabilidades, consulte a Política de Segurança.
1. Autenticação
Hashing de senhas — PBKDF2
| Propriedade | Valor |
|---|---|
| Algoritmo | PBKDF2 (Password-Based Key Derivation Function 2) |
| Função de hash | SHA-256 |
| Iterações | 100.000 |
| Formato de armazenamento | pbkdf2:<hexSalt>:<hexHash> |
| Salt | Aleatório, 32 bytes, gerado via crypto.getRandomValues |
| Local | workers/src/modules/auth/password.ts |
As 100k iterações foram escolhidas para manter compatibilidade com o CPU budget de 10 ms do Cloudflare Workers Free Tier. O backend legado (Fastify) usava scrypt, que se mostrou incompatível com o runtime Workers — a migração foi resolvida na ADR-0003.
Cada hash contém um salt aleatório independente, impedindo ataques de rainbow table mesmo que dois usuários usem a mesma senha.
Tokens JWT
| Token | Algoritmo | TTL | Biblioteca |
|---|---|---|---|
| Access Token | JWT HS256 | 15 minutos | jose |
| Refresh Token | Opaco (SHA-256 do hash armazenado) | 7 dias | jose |
O segredo de assinatura (JWT_SECRET) é configurado como variável de ambiente da Cloudflare (wrangler.toml / .dev.vars). Nunca é exposto ao frontend nem commitado no repositório.
Rotação de refresh token
Cada uso do refresh token invalida o anterior e gera um novo par. Isso detecta reuso malicioso: se um refresh token já revogado for apresentado, a sessão é marcada como suspected_compromise e todos os tokens daquele usuário são revogados. O frontend reage tentando reautenticação completa.
CSRF
Em modo Bearer (padrão), a proteção CSRF é inerente — o header Authorization: Bearer não é enviado automaticamente por navegadores em navegação cross-origin. O header X-CSRF-Token é mantido como defesa adicional, gerado por sessão e validado no backend.
2. Criptografia em repouso
Dados sensíveis armazenados localmente no IndexedDB são protegidos com criptografia AES-GCM de 256 bits.
O que é criptografado
| Dado | Campo | Local |
|---|---|---|
| Payload de alteração de estudante | encryptedChangePayload | studentEvents (IndexedDB) |
| Justificativa de exclusão | encryptedJustification | studentEvents (IndexedDB) |
| Refresh token + CSRF token | encryptedSessionSecrets | sessions (IndexedDB) |
| Hash da senha (credenciais) | encryptedCredentials | users (IndexedDB) |
Derivação de chave
- O usuário faz login com email + senha.
- O frontend calcula
SHA-256(senha)— a senha em texto plano nunca é armazenada. - Uma chave AES-GCM de 256 bits é derivada via PBKDF2 (600k iterações no frontend,
encryptionService.ts) a partir do hash da senha. - A chave é mantida em memória (
Map<userId, KeyContext>) e nunca persiste em disco. - Ao fazer logout ou expirar a sessão, a chave é removida da memória.
Versionamento e rotação
Cada envelope criptografado carrega um keyVersion. Quando o usuário faz logout/login, a versão da chave é incrementada e todos os envelopes da versão antiga são reencriptados com a nova chave (rotateEncryptedDataForUser). Se a versão do envelope não bater com a versão em cache, o sistema lança KEY_VERSION_MISMATCH, forçando rederivação.
Consulte a ADR-0004 para a decisão completa de arquitetura.
3. Segurança de rede
HTTPS
Toda comunicação entre frontend e backend é exclusivamente HTTPS. O header Strict-Transport-Security (HSTS) é configurado com max-age=63072000; includeSubDomains; preload, garantindo que navegadores nunca tentem HTTP para o domínio.
Content Security Policy (CSP)
O CSP é gerado em build por scripts/generate-headers.ts (executado no pnpm build:app) e gravado em app/public/_headers:
- Produção: CSP estrito baseado em hashes SHA-256 (sem
unsafe-inline, sem nonces) — o<style>inline doindex.htmlé permitido via hash. - Dev: CSP com nonce para os scripts inline do Vite HMR.
Evolução do CSP:
| Versão | Abordagem | Detalhes |
|---|---|---|
| v0.52.0 | SHA-256 hashes | Nonces substituídos por hashes pré-computados |
| v0.54.1 | 'unsafe-inline' | Cloudflare edge transforms quebravam hashes |
| v0.55.0 | Dynamic nonces via Middleware | HTMLRewriter + Pages Middleware (functions/_middleware.ts) — removido; substituído por CSP hash-based estático via _headers |
Origens no connect-src: https://api.neemias.app, https://challenges.cloudflare.com, https://fonts.googleapis.com, https://raw.githubusercontent.com.
PBKDF2 Password Hashing
Desde v0.53.0, o Neemias usa PBKDF2-600k (600.000 iterações, SHA-256) para hashing de senhas, tanto no backend (Workers) quanto no frontend (Web Crypto). O formato do hash armazenado é pbkdf2:<salt>:<derived>.
A comparação usa timingSafeEqual() (XOR + OR) para prevenir timing attacks.
Implementado via middleware security-headers.ts em toda resposta do Worker:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self';
img-src 'self' data: https:; connect-src 'self'; font-src 'self' data:;
object-src 'none'; base-uri 'self'; frame-ancestors 'none'Política restritiva: scripts, estilos e conexões apenas do mesmo domínio. Imagens permitem data: URIs (para avatares inline) e https: (para fotos de perfil externas). object-src 'none' e frame-ancestors 'none' previnem ataques de plugin e clickjacking.
Headers de segurança adicionais
| Header | Valor |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
X-XSS-Protection | 0 (desabilitado em favor da CSP) |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=(), interest-cohort=() |
CORS
Configurado com origens explícitas via cors.ts. O header Access-Control-Allow-Origin reflete a origem da requisição (não usa wildcard * em produção). Headers permitidos: Authorization, Content-Type, X-Device-Id, X-Csrf-Token, X-Idempotency-Key. Preflight (OPTIONS) responde com 204 No Content e cache de 24 horas (Access-Control-Max-Age: 86400).
4. Segurança da API
Idempotência obrigatória
Toda mutação (POST, PATCH) exige o header Idempotency-Key com um UUID v4. O backend mantém um ledger em D1 (idempotency_ledger) indexado por (key, user_id) com hash SHA-256 do payload. Isso garante que:
- Requisições repetidas com a mesma chave e mesmo payload retornam a resposta original (idempotente).
- Chave igual com payload diferente retorna HTTP
409 Conflict(detecção de replay malicioso). - Timeouts de rede e retentativas não causam duplicação de dados.
Consulte o guia de Idempotência para detalhes completos.
Header X-Device-Id
Toda requisição autenticada deve incluir o header X-Device-Id. O middleware de autenticação (auth.ts) rejeita requisições sem este header com HTTP 400. Isso vincula tokens a dispositivos específicos, dificultando o reuso de tokens roubados em outros dispositivos.
Rate limiting
Rate limiting é implementado com persistência em D1 (rate_limits) para compartilhar contadores entre edge locations da Cloudflare. Configuração atual:
| Rota | Janela | Máximo de requisições |
|---|---|---|
POST /api/v1/auth/login | 60 segundos | 5 (via middleware pipeline) |
POST /api/v1/auth/refresh | 60 segundos | 10 (via middleware pipeline) |
Limites excedidos retornam HTTP 429 Too Many Requests com header Retry-After. O sistema faz fail-open: se o D1 estiver indisponível, as requisições são permitidas (a WAF da Cloudflare atua como proteção primária). Entradas expiradas são limpas periodicamente a cada ~100 verificações.
Segredos
Todas as credenciais e segredos são injetados via variáveis de ambiente da Cloudflare:
- Desenvolvimento local:
.dev.vars(gitignorado, nunca commitado). - Produção: Cloudflare Secrets (dashboard ou
wrangler secret put). - Build-time:
DEPLOY_ENVinjetado via Vitedefine, sem prefixoVITE_para evitar conflitos comimport.meta.env.
5. Proteção de dados
Soft-delete com justificativa
Estudantes nunca são removidos fisicamente do banco. A operação de exclusão (DELETE /api/v1/students/:id) faz um soft-delete: atualiza status = 'DELETED' e registra deleted_justification. Um evento imutável SOFT_DELETE é criado na tabela unificada events via createCommandHandler → applyEvent. Isso preserva a trilha de auditoria e atende aos requisitos de rastreabilidade da LGPD.
Trilha de auditoria
A tabela audit_logs registra toda operação relevante com os campos:
| Campo | Descrição |
|---|---|
audit_id | UUID da entrada de auditoria |
correlation_id | ID de correlação da requisição |
actor_user_id | Usuário que executou a ação |
actor_role | Role do usuário no momento |
endpoint | Rota acessada |
outcome | Resultado (success, error, etc.) |
details | Payload JSON com detalhes da operação |
created_at | Timestamp ISO 8601 |
Eventos de chamada (attendance_events) e eventos de estudante (student_events) são imutáveis — uma vez criados, nunca são alterados ou removidos.
Classificação de dados PII (LGPD)
9 campos do estudante são classificados como PII: displayName, photoRef, guardianName, guardianNameAlt, birthDate, phones, address.*, allergies, specialNeeds. Consulte a página LGPD para o detalhamento completo das medidas de conformidade.
6. Infraestrutura
Cloudflare WAF
O Web Application Firewall da Cloudflare fornece uma camada adicional de proteção contra ataques comuns (SQL injection, XSS, DDoS) antes que as requisições cheguem ao Worker. O rate limiting no Worker atua como segunda linha de defesa.
Backups automáticos (D1)
O Cloudflare D1 realiza snapshots automáticos point-in-time, permitindo restaurar o banco para qualquer momento dentro da janela de retenção. Backups diários e cópias semanais são gerenciados pela plataforma — não exigem configuração adicional. Consulte Backup e Restauração.
Segredos e ambiente
| Segredo | Local | Propósito |
|---|---|---|
JWT_SECRET | Cloudflare Secrets / .dev.vars | Assinatura de tokens JWT |
SEED_PASSWORD | .dev.vars (dev apenas) | Senha dos usuários de seed |
CLOUDFLARE_API_TOKEN | CI Secrets / .dev.vars | Deploy via Wrangler |
VITE_GOOGLE_MAPS_API_KEY | .env.local (frontend) | Google Maps Places API |
DEPLOY_ENV | Cloudflare Pages Secrets | Ambiente de deploy (dev/staging/prod) |
security.txt
Implementado conforme RFC 9116. Disponível em /.well-known/security.txt, permite que pesquisadores de segurança encontrem facilmente o canal correto para reportar vulnerabilidades.
Resumo dos findings da auditoria
A auditoria de segurança identificou 14 issues, todas resolvidas:
| Severidade | Issues | Status |
|---|---|---|
| 🔴 CRITICAL | 3 (JWT hardening, proteção do _seed, timingSafeEqual) | ✅ Resolvidas |
| 🟠 HIGH | 4 (sessionStorage, dangerousInnerHTML, rate limiting, error leak) | ✅ Resolvidas |
| 🟡 MEDIUM | 5 (device ID, CSRF, role sync, token notificação, CORS) | ✅ Resolvidas / Decidido não implementar |
| 🔵 LOW | 2 (cookie secure, security.txt) | ✅ Resolvidas |
| ⚪ INFO | 1 (JSX rendering) | ✅ wontfix |
Referências
- Política de Segurança — como reportar vulnerabilidades
- ADR-0003 — migração scrypt → PBKDF2
- ADR-0004 — criptografia client-side com AES-GCM
- ADR-0007 — validação de tokens JWT
- Arquitetura de Autenticação — fluxo completo de auth
- Guia de Idempotência — ledger de idempotência
- LGPD — conformidade com a LGPD
- STATE.md — histórico completo da auditoria