# Segurança (/docs/seguranca)



O modelo de segurança do Covil é de **defesa em profundidade**: cada operação
sensível é protegida em mais de uma camada, de modo que uma falha isolada não
vira brecha. Este documento descreve o modelo e as regras que **não** podem ser
violadas.

## As duas camadas [#as-duas-camadas]

1. **Guarda na aplicação** — Server Actions e páginas admin verificam
   autenticação **e** autorização (papel + seção) antes de qualquer efeito.
2. **RLS no Postgres** — mesmo que a camada 1 falhe ou seja contornada, as
   *policies* do banco barram leitura/escrita indevida. A RLS é o anteparo final.

A lógica crítica (associação, pontos, cupons, carteirinha) fica em funções
`SECURITY DEFINER` no banco, que validam o papel por dentro — ver
[`banco-de-dados.md`](banco-de-dados.md).

## Regras invioláveis [#regras-invioláveis]

### 1. O cliente nunca é confiável [#1-o-cliente-nunca-é-confiável]

`createCheckoutAction` ([`checkout-actions.ts`](../src/lib/actions/checkout-actions.ts))
rebusca cada produto/ingresso no banco, reaplica o preço de sócio, **revalida o
cupom no servidor** (cupom pessoal só vale para o dono) e recomputa o total. O
preço/total vindo do cliente serve só para exibição. Estoque e limite por pedido
de ingresso também são validados no servidor.

### 2. Toda ação admin tem guarda de papel e seção [#2-toda-ação-admin-tem-guarda-de-papel-e-seção]

Escritas admin passam por `getAdminClient`/`getPrivilegedClient`, que chamam
`requireSectionAccess` (papel tem acesso àquela seção?) antes de tocar o banco.
O mapa `SECTION_BY_PATH` em [`admin-actions.ts`](../src/lib/actions/admin-actions.ts)
é **fail-closed**: se um path não estiver mapeado, o acesso é **negado** (não
liberado para "qualquer admin").

### 3. Service role isolado no servidor [#3-service-role-isolado-no-servidor]

[`supabase/admin.ts`](../src/lib/supabase/admin.ts) usa a *service role key*, que
**ignora a RLS**. Ele tem `import 'server-only'` e só deve ser usado no webhook e
em escritas de pedido no servidor. Nunca o importe em Client Component — o
`next build` quebra de propósito se isso acontecer.

### 4. Webhook de pagamento confirmado por reconsulta autoritativa [#4-webhook-de-pagamento-confirmado-por-reconsulta-autoritativa]

[`webhook/pix/route.ts`](../src/app/api/sicoob/webhook/pix/route.ts):

* **Não há assinatura HMAC nem segredo de webhook.** A garantia é a **reconsulta
  autoritativa da cobrança por `txid` na API do Sicoob** (`getPixCharge`): o
  pedido só vira `paid` se a cobrança estiver `CONCLUIDA` na própria API.
* Por isso um **corpo forjado não confirma pagamento** — a fonte da verdade é a
  API do Sicoob, não o corpo recebido no webhook.
* O processamento é **idempotente**: se o status efetivo não muda, sai cedo;
  `earn_points`/`reverse_purchase_points` não duplicam efeito.

### 5. Segredos nunca vazam para o cliente [#5-segredos-nunca-vazam-para-o-cliente]

Só variáveis `NEXT_PUBLIC_*` chegam ao browser. Segredos —
`SUPABASE_SERVICE_ROLE_KEY`, `SICOOB_*`, `GMAIL_*`, `CARD_SIGNING_SECRET` —
**não** têm esse prefixo e são lidos apenas no servidor
([`env.ts`](../src/lib/env.ts)). Nunca commite `.env.local`.

### 6. Dados sensíveis do perfil são imutáveis pelo cliente [#6-dados-sensíveis-do-perfil-são-imutáveis-pelo-cliente]

Triggers no banco tornam `role`, `status`, `points_balance` e os campos
`membership_*` **somente-leitura** via cliente (`guard_*`). Alterações passam por
funções `SECURITY DEFINER` (ex.: `assign_membership_card`, `adjust_points`).

### 7. Ledger de pontos imutável [#7-ledger-de-pontos-imutável]

Saldo de pepitas = soma de `loyalty_transactions` (append-only). Correção é uma
nova transação (`adjust`/`reversal`), nunca `UPDATE`/`DELETE`. O
`profiles.points_balance` é só um cache reconciliável.

### 8. Verificação pública da carteirinha sem PII [#8-verificação-pública-da-carteirinha-sem-pii]

`verify_membership_card(token)` (executável por `anon`) devolve **apenas** nome,
foto, número, status, período e plano — nunca e-mail, telefone ou RA. O código de
autenticidade é um HMAC ([`membership-card.ts`](../src/lib/membership-card.ts))
reexibido para conferência visual.

## Privacidade / open redirect [#privacidade--open-redirect]

* `normalizeNextPath` barra open redirect no login (rejeita `//host`, `/\host`).
* Reset de senha responde de forma **uniforme** (anti-enumeração de e-mail).
* Não coloque dados pessoais em URL/query string.

## Histórico de auditoria [#histórico-de-auditoria]

* **2026-08-07** — auditoria de segurança. Corrigidos no código + migration
  `20260807120000_security_hardening.sql`: remoção de auto-escalada a
  `super_admin`; `activate_membership`/`redeem_coupon` deixaram de ser
  executáveis por `public`; separação de `is_admin()`; cupons não legíveis por
  `anon`; storage com escopo de dono; correção de open redirect.
* **2026-08-11** — correções de robustez: validação de estoque/limite de
  ingresso no checkout (anti-oversell), unificação do cálculo de pepitas no
  webhook e autorização admin **fail-closed**.

> ⚠️ **Pendências operacionais**: confirme em produção que a migration
> `20260807120000_security_hardening.sql` foi aplicada e que os segredos estão
> configurados (`CARD_SIGNING_SECRET`, `SICOOB_*`). Ver
> [`deploy.md`](deploy.md).
