Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions audit/AUDITORIA_FRONTEND_MCP_2026-05-24.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
**Sessão:** login `adm01@promobrindes.com.br` — papel **Supervisor** (usuário "Joaquim Ataides")
**Método:** navegador remoto via MCP (sessão persistente), captura de rede, snapshots ARIA, screenshots + análise de código-fonte + advisors do Supabase.

> **Atualização 2026-05-24 (Rodada 2 — verificação aprofundada + correções):** ver seção [Rodada 2](#rodada-2--verificação-aprofundada-e-resolução) no final. Resumo: RLS confirmada ativa em **todas as 280 tabelas**; das 309 políticas anon/public, apenas **18 são irrestritas** e **nenhuma** expõe dado sensível. 2 funções internas tiveram EXECUTE revogado de anon/authenticated e foi adicionado rate-limit ao reset de senha (migration `20260524210000`).

---

## Resumo executivo
Expand Down Expand Up @@ -108,3 +110,33 @@ Em `/dashboard`, o widget abaixo de "Suas Métricas do Mês" permaneceu em skele
5. Ligar o Kit Maker a dados reais (ou marcar claramente como demo).
6. Calibrar o threshold de validade de preço.
7. Executar `get_advisors(performance)` (não rodado nesta sessão) para fechar o lado de performance.

---

## Rodada 2 — verificação aprofundada e resolução

Após a primeira passada, cada achado de backend foi **verificado com SQL read-only** antes de qualquer ação, para distinguir risco real de ruído de advisor.

### Postura de RLS (medida, não presumida)
| Métrica | Valor |
|---|---|
| Tabelas `public` com **RLS desabilitada** | **0** |
| Tabelas `public` com RLS habilitada | 280 |
| Políticas para os papéis `anon`/`public` | 309 |
| Dessas, com expressão **irrestrita** (`USING/CHECK = true`) | **18** |
Comment on lines +121 to +126

As 18 políticas irrestritas são **todas** de dados não sensíveis: catálogo/refs (`products`, `categories`, `color_groups`, `material_equivalences`, `product_variants`, `product_relationships`, `product_*_packagings`, `supplier_colors`, `variant_commemorative_dates`, `commemorative_date_*`) e essenciais pré-login (`system_kill_switches`, `geo_allowed_countries`) — estes últimos lidos por `anon` **por design** (ver commit #297 "kill_switch.ts usa anon"). As tabelas sensíveis (`admin_audit_log`, `admin_settings`, `access_security_settings`, `auth_login_attempts`) **não** aparecem na lista irrestrita — têm políticas devidamente condicionadas. **Conclusão: não há vazamento de dado ativo;** os 373 avisos `anon_table_exposed` são superfície de API mitigada por RLS. Revogação em massa dos grants foi **descartada** (alto risco de quebrar catálogo/quote público/pré-login, ganho real nulo).

### Corrigido nesta rodada (migration `20260524210000_harden_anon_grants_and_password_reset_rate_limit.sql`)
1. **`REVOKE EXECUTE`** de `cleanup_expired_webhook_request_nonces()` e `get_public_schema_signatures()` para `anon` e `authenticated`. Verificado que **nenhuma** é chamada pelo front-end (`src/`) nem por edge functions; `postgres`/`service_role` mantêm acesso (pg_cron e funções server-side seguem operando). SQL validado em transação com `ROLLBACK`.
2. **Rate-limit no reset de senha** — trigger `BEFORE INSERT` (`SECURITY DEFINER`, necessário pois `anon` não tem `SELECT`) limita a **3 solicitações por e-mail a cada 60 min** em `password_reset_requests`, fechando o vetor de abuso da política de INSERT irrestrita (achado #4) sem removê-la (ela é necessária ao fluxo pré-login).

### Reclassificado após investigação (não são defeitos de código — *não* alterados)
- **`get_quote_token_by_value` por anon** — é o lookup **por token-segredo** do orçamento público compartilhável (precisa do segredo para resolver). Não é enumeração; **mantido**.
- **"Preço próximo do limite de validade" em todos os produtos** — `src/utils/price-freshness.ts` está **correto** (stale > 60d, aging > 30d). O aviso aparecer em tudo significa que os preços do catálogo (SSOT externo) estão genuinamente com > 30 dias. **Remédio = atualização de preços na origem (data ops)**, não código. Mexer no threshold apenas **mascararia** um sinal válido — descartado.
- **Widget "Próximas Datas" do Dashboard** — `UpcomingDatesWidget` + hook `useUpcomingCommemorativeDates` têm estados de loading/erro/vazio corretos. O skeleton observado era carregamento **transitório** (screenshot logo após navegar), sem defeito confirmado.

### Pendências que exigem decisão de produto / dados (não executáveis com segurança aqui)
- **Kit Maker servindo `MOCK_BOXES/MOCK_ITEMS`** (`src/lib/kit-builder/index.ts` → `mock-data.ts`). Conectar a dados reais é **implementação de feature** + fonte de dados a definir; fabricar uma tabela seria arriscado. **Requer decisão de produto.**
- **Atualização de preços do catálogo** (ver acima) — operação de dados na origem.
- **`get_advisors(performance)`** — recomendado rodar para fechar o lado de performance.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
-- Hardening derivado da auditoria de segurança via MCP (2026-05-24).
--
-- Achados tratados:
-- 1. Funções SECURITY DEFINER internas executáveis por anon/authenticated.
-- - cleanup_expired_webhook_request_nonces(): rotina de manutenção, deve
-- rodar apenas via pg_cron (postgres) / service_role.
-- - get_public_schema_signatures(): introspecção de schema, não deve ser
-- chamável pelo cliente. Nenhuma das duas é invocada pelo front-end
-- (verificado em src/ e supabase/functions/).
-- postgres e service_role mantêm EXECUTE — pg_cron e edge functions seguem
-- funcionando.
-- 2. password_reset_requests tinha política de INSERT irrestrita ("Anyone can
-- request a password reset", WITH CHECK true) sem qualquer limite de taxa.
-- Como anon NÃO tem SELECT na tabela, o limite precisa de uma função
-- SECURITY DEFINER para contar solicitações recentes. Limite: 3 por e-mail
-- a cada 60 minutos (mitiga flood/abuso sem bloquear uso legítimo).

-- 1. Revoga EXECUTE das funções internas dos papéis expostos via API ------------
REVOKE EXECUTE ON FUNCTION public.cleanup_expired_webhook_request_nonces()
FROM anon, authenticated;
REVOKE EXECUTE ON FUNCTION public.get_public_schema_signatures()
FROM anon, authenticated;

-- 2. Rate-limit de solicitações de reset de senha ------------------------------
CREATE OR REPLACE FUNCTION public.enforce_password_reset_rate_limit()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path TO 'public', 'pg_temp'
AS $function$
DECLARE
recent_count integer;
window_minutes constant integer := 60;
max_requests constant integer := 3;
BEGIN
-- Conta solicitações recentes do mesmo e-mail (case-insensitive).
-- SECURITY DEFINER é necessário porque anon não possui SELECT na tabela.
SELECT count(*) INTO recent_count
FROM public.password_reset_requests
WHERE lower(email) = lower(NEW.email)
AND requested_at > now() - make_interval(mins => window_minutes);
Comment on lines +38 to +41

IF recent_count >= max_requests THEN
RAISE EXCEPTION 'Muitas solicitações de redefinição de senha. Tente novamente mais tarde.'
USING ERRCODE = 'check_violation';
END IF;
Comment on lines +38 to +46

RETURN NEW;
END;
$function$;

DROP TRIGGER IF EXISTS trg_password_reset_rate_limit ON public.password_reset_requests;
CREATE TRIGGER trg_password_reset_rate_limit
BEFORE INSERT ON public.password_reset_requests
FOR EACH ROW
EXECUTE FUNCTION public.enforce_password_reset_rate_limit();
Loading