Segurança: 5 tabelas saem de baixo da chave pública + search_path em 39 funções - #54
Merged
Conversation
O cruzamento banco × código do db/ESTRUTURA.md achou 5 das 112 tabelas com RLS DESLIGADA e GRANT ALL TO anon. A chave anônima vai no bundle do frontend: sem RLS, qualquer um com ela lê e escreve nessas tabelas. Hoje não vaza nada — as 5 estão com zero linhas. Mas employees guarda salario_bruto, benefícios e cpf_mascara, e tem 17 usos em src/ (Custo Hora-Homem). No dia que a feature rodar, folha de pagamento real fica legível e gravável por qualquer um. Por isso entra agora: com as tabelas vazias e sem tráfego, não há o que quebrar. employees, company_ai_dossiers e consultant_memories ganham o padrão de ingestion_documents (o caso análogo, cliente escreve no próprio projeto): admin, staff ATR, ou cliente com acesso AO projeto. Detalhe: consultant_memories já tinha comentário no código dizendo "a RLS barraria o insert sem sessão" — o código acreditava na RLS que o banco não tinha. categorization_patterns é catálogo global, sem project_id: logado lê, só a equipe escreve. Aqui morava uma armadilha — increment_pattern_hit é plpgsql INVOKER e dá UPDATE nessa tabela; com RLS ligada o contador morreria EM SILÊNCIO (UPDATE que casa zero linhas não dá erro). Em vez de abrir UPDATE pra todo mundo, a função vira SECURITY DEFINER: é o caminho de escrita controlado, e só sabe somar 1 num id. CREATE OR REPLACE preserva os GRANTs e o retorno void não muda. meetings fica com RLS sem policy — zero linhas, zero referências em src/. Pra tabela que ninguém lê, policy seria adivinhação; quando a feature existir, a policy nasce com ela e o primeiro acesso falha alto, não calado. Junto, search_path fixo em 39 funções SECURITY DEFINER: as 29 que não tinham nenhum, mais 10 que tinham só 'public'. Essas 10 pareciam protegidas e não estavam — sem pg_temp explícito, o Postgres procura o schema temporário PRIMEIRO. Conferido antes de escrever: nenhuma das 39 chama função de extensão sem qualificar schema, nenhuma tem overload, e as 39 assinaturas foram diffadas contra o dump. ALTER FUNCTION não toca corpo, assinatura nem GRANT. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Updates to Preview Branch (fix/rls-tabelas-orfas) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
Verificação no código vivo, pedida antes de aplicar: dos três caminhos que eu disse que quebrariam sem policy, só dois estão vivos. CHH e Folha estão de pé — rota /financeiro/chh e /financeiro/folha, alcançáveis pela aba Pessoas (flag FINANCEIRO_CASA_GRUPO global ON) via DrillLink no PainelCasaTab, mais command-center, AnalysisRecommendationsPanel e um import dinâmico em treasury-week-service. Auto-categorização NÃO. `auto-categorizer-enhanced.ts` é código morto: nenhum dos 3 exports é importado em lugar nenhum. A categorização viva é `categorization-rules.ts`, e ela não toca categorization_patterns. Nada muda no SQL — as policies e o SECURITY DEFINER seguem certos, e desarmar a armadilha antes de o módulo reviver custa zero. Muda o comentário, que dizia "armadilha tratada" onde o honesto é "armadilha latente". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Efeito colateral da 20260812000000, achado no re-dump — que é exatamente o que o
re-dump serve pra achar.
employees NÃO era tabela sem policy: já tinha SEIS, dormentes, porque policy
existe mesmo com RLS desligada e só não é aplicada. Meu F0 perguntou "a RLS está
ligada?" e não perguntou "que policies já existem?". O DROP POLICY IF EXISTS
substituiu 4 delas às cegas.
Três substituições não ampliaram nada. select/insert/update antigas dependiam de
project_members — tabela VAZIA, e client_project_access é view sobre ela filtrando
role='cliente'. E as policies employees_cliente_insert/_cliente_update, que não
foram tocadas, já traziam a mesma cláusula de staff que escrevi. Sem essa cláusula
no SELECT, o CHH pararia de enxergar pro consultor no instante em que a RLS ligou.
A quarta ampliou de verdade: employees_delete era is_admin() puro — role IN
('owner','admin') — e passou a deixar CONSULTOR apagar linha de folha de
pagamento. Não foi decisão, foi descuido; a restrição original era deliberada e
nenhum caminho vivo precisa dela (upload de folha faz INSERT/UPDATE). Volta ao
que era.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… a ser autoritativo schema.sql 14.899 → 15.030. Conferido objeto a objeto: tabelas 112, views 12, funções 73 e triggers 92 inalterados; policies 348 → 360; tabelas com RLS 107 → 112 de 112; SECURITY DEFINER sem search_path 29 → 0. roles.sql com diff vazio. seed.sql 25.164 → 25.175, nenhuma tabela perdeu linha. Foi este re-dump que achou o efeito colateral corrigido pela 20260812010000 — esperávamos +16 policies e vieram +12, e a diferença revelou as 6 policies dormentes de employees. Migration não mostra o que ela mesma fez; dump mostra. Corrige também um número que foi afirmado antes de aplicar. Eu disse que as 5 tabelas estavam com ZERO linhas; o método de contagem procurava blocos COPY e este dump usa INSERT, então voltava zero pra tudo. Contagem certa: employees 0, meetings 0, consultant_memories ~4, company_ai_dossiers ~5, categorization_patterns ~33. Conferido no dado que ninguém perdeu acesso: profiles tem 1 owner + 2 admin (cobertos por is_admin) e 3 cliente, e project_members tem exatamente 3 linhas role='cliente' pra eles — que é o que a view client_project_access enxerga. O 4º membro é consultor no projeto mas owner em profiles, coberto pelas duas pontas. O log completo dessa validação está no SNAPSHOT.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Traz o gate de db/ESTRUTURA.md do #53 para que ele valide justamente este PR, que mexe no dump e no mapa gerado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item 3 da lista aberta pelo marco zero (#52). Primeira migration a passar pela rede nova — o Supabase Preview replayou baseline + esta migration do zero e passou em 18s.
O buraco
db/ESTRUTURA.mdachou 5 das 112 tabelas com RLS desligada eGRANT ALL TO anon. A chave anônima é pública, vai no bundle do frontend.Hoje não vaza nada: as 5 estão com zero linhas. Mas
employeesguardasalario_bruto, benefícios ecpf_mascara. No dia que a feature rodar, folha de pagamento real fica legível — e gravável — por qualquer um com a chave pública.É por isso que entra agora: com as tabelas vazias e sem tráfego, não há o que quebrar.
Quem realmente lê estas tabelas (verificado no código vivo)
Mapeado antes de escrever a policy. Achado que decidiu tudo: nenhum leitor usa
service_role— todos vão por@/lib/supabase/clientou/server, os dois com chave anon + sessão. RLS sem policy quebraria caminho vivo./financeiro/chh)DrillLink"Custo hora-homem" noPainelCasaTab:275; flagFINANCEIRO_CASA_GRUPOglobal ON. Tambémcommand-center:473,AnalysisRecommendationsPanel:47e import dinâmico emtreasury-week-service:96/financeiro/folha)PainelCasaTab:274+SubirApoioModal:175. Escreveemployeesno uploadauto-categorizer-enhanced.ts— nenhum dos 3 exports (autoCategorize,autoCategorizeTransacoes,getCategorizationStats) é importado em lugar nenhum. A categorização viva écategorization-rules.ts, que não tocacategorization_patternsSem policy, o efeito nos dois vivos seria assimétrico e traiçoeiro: upload de folha daria erro (INSERT barrado), e CHH mostraria "sem colaboradores" para sempre — SELECT barrado por RLS volta vazio, não volta erro.
O que cada tabela ganhou
employeesingestion_documents(20260617) — o caso análogo, cliente escreve no próprio projetocompany_ai_dossiersconsultant_memorieslearning-memory.ts:30traz "a RLS de consultant_memories barraria o insert sem sessão"categorization_patternsproject_id. Não é dado de tenantmeetingssrc/. Policy para tabela que ninguém lê é adivinhaçãoA armadilha latente do contador
increment_pattern_hitéplpgsqlINVOKER e dáUPDATEemcategorization_patterns. Com RLS ligada e sem policy de UPDATE pro usuário comum, o contador morreria em silêncio —UPDATEque casa zero linhas não levanta erro.Hoje ela não morde, porque o único chamador é o módulo morto acima. Desarmar antes de uma eventual revivência custa zero: em vez de abrir UPDATE pra todo mundo (qualquer um reescrevendo o catálogo), a função vira
SECURITY DEFINER— caminho de escrita controlado, que só sabe somar 1 nohit_countde um id.CREATE OR REPLACEpreserva os GRANTs e o retornovoidnão muda.search_pathem 39 funções (não 29)As 29 sem
search_pathnenhum, mais 10 que tinham só'public'. Essas 10 pareciam protegidas: sempg_tempexplícito, o Postgres procura o schema temporário primeiro. O gerador só testa presença, por isso passavam como ✅.search_pathnão quebra nenhumaALTER FUNCTIONnão toca corpo, assinatura nem GRANTAplicação
Migration idempotente (
DROP POLICY IF EXISTSantes de cadaCREATE), fecha comNOTIFY pgrst. Ainda não aplicada — segue o fluxo da casa: aplicar no SQL Editor, depois re-dump enpx tsx scripts/db/gerar-estrutura.ts, e oESTRUTURA.mdpassa a mostrar 112/112 com RLS.🤖 Generated with Claude Code