Skip to content

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

PropriedadeValor
AlgoritmoPBKDF2 (Password-Based Key Derivation Function 2)
Função de hashSHA-256
Iterações100.000
Formato de armazenamentopbkdf2:<hexSalt>:<hexHash>
SaltAleatório, 32 bytes, gerado via crypto.getRandomValues
Localworkers/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

TokenAlgoritmoTTLBiblioteca
Access TokenJWT HS25615 minutosjose
Refresh TokenOpaco (SHA-256 do hash armazenado)7 diasjose

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

DadoCampoLocal
Payload de alteração de estudanteencryptedChangePayloadstudentEvents (IndexedDB)
Justificativa de exclusãoencryptedJustificationstudentEvents (IndexedDB)
Refresh token + CSRF tokenencryptedSessionSecretssessions (IndexedDB)
Hash da senha (credenciais)encryptedCredentialsusers (IndexedDB)

Derivação de chave

  1. O usuário faz login com email + senha.
  2. O frontend calcula SHA-256(senha)a senha em texto plano nunca é armazenada.
  3. Uma chave AES-GCM de 256 bits é derivada via PBKDF2 (600k iterações no frontend, encryptionService.ts) a partir do hash da senha.
  4. A chave é mantida em memória (Map<userId, KeyContext>) e nunca persiste em disco.
  5. 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 do index.html é permitido via hash.
  • Dev: CSP com nonce para os scripts inline do Vite HMR.

Evolução do CSP:

VersãoAbordagemDetalhes
v0.52.0SHA-256 hashesNonces substituídos por hashes pré-computados
v0.54.1'unsafe-inline'Cloudflare edge transforms quebravam hashes
v0.55.0Dynamic nonces via MiddlewareHTMLRewriter + 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

HeaderValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection0 (desabilitado em favor da CSP)
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), 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:

RotaJanelaMáximo de requisições
POST /api/v1/auth/login60 segundos5 (via middleware pipeline)
POST /api/v1/auth/refresh60 segundos10 (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_ENV injetado via Vite define, sem prefixo VITE_ para evitar conflitos com import.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 createCommandHandlerapplyEvent. 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:

CampoDescrição
audit_idUUID da entrada de auditoria
correlation_idID de correlação da requisição
actor_user_idUsuário que executou a ação
actor_roleRole do usuário no momento
endpointRota acessada
outcomeResultado (success, error, etc.)
detailsPayload JSON com detalhes da operação
created_atTimestamp 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

SegredoLocalPropósito
JWT_SECRETCloudflare Secrets / .dev.varsAssinatura de tokens JWT
SEED_PASSWORD.dev.vars (dev apenas)Senha dos usuários de seed
CLOUDFLARE_API_TOKENCI Secrets / .dev.varsDeploy via Wrangler
VITE_GOOGLE_MAPS_API_KEY.env.local (frontend)Google Maps Places API
DEPLOY_ENVCloudflare Pages SecretsAmbiente 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:

SeveridadeIssuesStatus
🔴 CRITICAL3 (JWT hardening, proteção do _seed, timingSafeEqual)✅ Resolvidas
🟠 HIGH4 (sessionStorage, dangerousInnerHTML, rate limiting, error leak)✅ Resolvidas
🟡 MEDIUM5 (device ID, CSRF, role sync, token notificação, CORS)✅ Resolvidas / Decidido não implementar
🔵 LOW2 (cookie secure, security.txt)✅ Resolvidas
⚪ INFO1 (JSX rendering)✅ wontfix

Referências

Distribuído sob licença MIT.