Central de Transparência
LGPD · Art. 46

Segurança da informação

As medidas técnicas, cada uma apontando para o código que a implementa.

Política de segurança da informação

Art. 46, 47, 49 e 50 da LGPD — medidas técnicas e administrativas aptas a proteger os dados pessoais.

Para que serve este documento

O art. 46 exige medidas de segurança; o art. 50 incentiva documentá-las como programa de governança. A diferença entre um documento útil e um teatral está em uma coisa: cada medida abaixo aponta para o código que a implementa. Uma política que afirma "adotamos criptografia forte" sem dizer onde não é verificável — e o que não é verificável não é auditável.

Este é o par de SECURITY.md (voltado a quem reporta vulnerabilidade); aqui o foco é a conformidade.


1. Controle de acesso

MedidaImplementaçãoOnde
Senha derivada com PBKDF2-SHA256, 100.000 iterações, salt de 128 bits por credencialhashPassword()src/utils.js
Comparação em tempo constantetimingSafeEqual()src/utils.js
Iteração gravada junto do hash (permite elevar o custo sem invalidar credenciais)formato pbkdf2:<iter>:<salt>:<hash>src/utils.js
Política de senha: mínimo 12, variedade de classes, rejeição de padrões previsíveisvalidatePassword()src/security.js
Sem "confiança na primeira execução": sem credencial e sem secret, o login é impossívelgetAdminHash()src/index.js
Hash executado mesmo sem credencial armazenada, para não vazar por tempo de resposta se o painel tem donohandleLogin()src/index.js
Rate limit em duas camadas: 10/10 min e 60/dia por IPcheckRateLimit()src/index.js
Alerta por e-mail a partir de 5 falhas em 15 minnoteFailedLogin() / sendLoginAlert()src/index.js, src/utils.js
Troca de senha revoga todas as outras sessõeshandleChangePassword()src/index.js

2. Sessão

MedidaImplementação
Token de 256 bits de CSPRNGgenerateToken()
Cookie __Host-session: HttpOnly, Secure, SameSite=Strict, Path=/, sem Domain — impede que outro host de lucafchala.com plante uma sessãosessionCookie()
Expiração absoluta de 24 hTTL do KV
Expiração por 2 h de inatividadeverifySession()
Vínculo ao cliente (hash do User-Agent); divergência encerra a sessãoclientFingerprint()
Renovação com trava de 10 min, para não consumir a cota de escrita do KVverifySession()
Clear-Site-Data no logout — apaga cache, cookies e storage do browserhandleLogout()

3. Proteção da aplicação

VetorMedida
XSSEscape canônico de 5 caracteres em toda interpolação; gate de CI proíbe a variante de 3 caracteres
XSS via URLsafeUrl() — allowlist de esquema aplicada no ponto de uso, não só na gravação, cobrindo dados legados e restaurados
XSSCSP com nonce por requisição em todos os <script>; CSP estrita em Report-Only com coletor em /api/csp-report
CSRFSec-Fetch-Site/Origin verificados antes do roteamento, para todo método que escreve. Recusa same-site, que o cookie SameSite=Strict sozinho aceitava
ClickjackingX-Frame-Options: DENY + frame-ancestors 'none'
MIME sniffingX-Content-Type-Options: nosniff em todas as respostas
Downgrade para HTTPHSTS de 2 anos com includeSubDomains; upgrade-insecure-requests
Injeção SQLPrepared statements com bind em 100% dos acessos ao D1
Injeção de fórmula em CSVcsvCell() neutraliza = + - @ TAB CR iniciais — os campos consenter_name, user_agent e referrer são controlados pelo visitante e abrem na planilha do controlador
Upload maliciosoVerificação de magic bytes (isLikelyImage), teto de 2 MB, nome de arquivo higienizado
Enumeração de projetosNonce de página assinado (HMAC), amarrado ao slug, validade de 2 h
BotsTurnstile fail-closed + honeypot + token de formulário com idade mínima
Vazamento por cacheno-store em toda resposta de dado; noindex e no-referrer no painel
Poluição de dados via restoreBackup restaurado é higienizado por chave, tipo e tamanho

4. Minimização (art. 6º, III)

Não é um controle acessório: é o que reduz o impacto de todos os incidentes de uma vez.

5. Criptografia

Em trânsitoTLS obrigatório (Cloudflare), HSTS de 2 anos, upgrade-insecure-requests
Em repousoCifrado pelo provedor: Cloudflare KV e D1, Google Drive. Sem camada adicional de cifra da aplicação
CredenciaisPBKDF2-SHA256 100k, salt por credencial. Senha em texto claro nunca é gravada nem registrada
SegredosCloudflare Secrets e GitHub Actions Secrets. Nunca no repositório — verificado por gate de CI

⚠️ Não há cifra de campo na aplicação sobre o log de consentimento. Justificativa: a chave teria de viver no mesmo ambiente que o dado (o Worker), o que protege contra vazamento do arquivo de banco mas não contra comprometimento da conta — que é o cenário realista aqui. A proteção efetiva é o controle de acesso da seção 1 e a retenção curta.

6. Detecção e resposta

SinalComo chega
Exceção não tratadaE-mail (sendErrorAlert), com cooldown global de 15 min
Força bruta no loginE-mail a partir de 5 falhas em 15 min
Tentativa de XSSRelatório de CSP em /api/csp-report → log estruturado
Cron morto em silênciocron:last + cron.stale em /api/healthz
Falha na poda de retençãoE-mail (antes só existia nos logs)
Configuração incompletaauditSite() acusa secret ausente — inclusive SIGNING_SECRET, cuja falta desliga controles sem quebrar nada
Disponibilidadestatus.lucafchala.com + /api/healthz

Procedimento completo: plano-resposta-incidentes.md.

7. Ciclo de desenvolvimento

ControleOnde
Lint obrigatóriochecks.yml
Suíte de testes obrigatória antes do deploychecks.yml, deploy.yml
Testes dedicados de segurançatests/security.test.js, tests/drive-gate.test.js
CodeQL security-extended, semanal e por PRsecurity.yml
npm audit com falha em high+security.yml
Revisão de dependência bloqueando PRsecurity.yml
Dependabot semanal (npm + Actions)dependabot.yml
Invariantes estruturais (portão de CSRF na posição certa, nenhum eval, nenhum secret literal, todo <script> com nonce)security.yml
Verificação dos cabeçalhos na resposta real de produçãosmoke test do deploy.yml
Menor privilégio no token dos workflowspermissions: contents: read

8. Divulgação responsável

security@lucafchala.com, com chave PGP, publicado em /.well-known/security.txt conforme RFC 9116. Política, escopo e prazo de resposta em SECURITY.md.

9. Limitações conhecidas — declaradas, não escondidas

Um documento de segurança que só lista acertos é propaganda. Estas são as fragilidades conhecidas, com o motivo de cada uma:

  1. Link do Drive é redistribuível. Inerente à entrega por Drive. Reduzir exigiria servir as fotos por rota própria, o que esbarra na cota de requisições do Worker.
  2. 'unsafe-inline' ainda vale para scripts. A UI usa handlers inline (onclick="…"), que nonce nenhum cobre. A política estrita já roda em Report-Only, medindo o que falta; a virada acontece quando os relatórios zerarem.
  3. Caminho sem JavaScript é mais fraco. Com o Turnstile bloqueado por ad-blocker, o cliente usa turnstileToken: "noscript". Continua sendo um POST por evento, com rate limit mais apertado e auditado com turnstile_ok=0. É uma escolha de acessibilidade, documentada.
  4. Sem segundo fator no painel. Registrado no TODO (magic link ou TOTP).
  5. Sem COEP. require-corp quebraria as imagens do lh3.googleusercontent.com, que não enviam CORP.
  6. HSTS sem preload. É compromisso de domínio inteiro, praticamente irreversível — decisão do dono, não efeito colateral de um commit.
  7. Anexo em formato que o servidor não consegue limpar é recusado, não enviado. A limpeza cobre JPEG, PNG, WebP, HEIC, AVIF e GIF; um arquivo fora do padrão desses contêineres é recusado com orientação de como reenviar, porque anexar sem apagar os metadados seria pior.

10. Governança