Auditoria RLS do Supabase
Esta skill executa a parte de banco de dados do scanner vibe-preflight (scripts/preflight.mjs ao lado deste arquivo,
Node 20+, sem dependências, sem rede). Ela lê os arquivos .sql do projeto (migrations do Supabase,
schema.sql), reproduz a ordem de tabelas, políticas, views, funções, grants e buckets de armazenamento, e reporta
9 problemas. Não se conecta a um banco de dados.
Como executar
A partir da raiz do projeto:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only database --format json
Claude Code preenche ${CLAUDE_SKILL_DIR}. Outros agentes: use a pasta desta skill, ex.:
.agents/skills/supabase-rls-audit ou .claude/skills/supabase-rls-audit.
Código de saída 1 significa pelo menos um erro. Adicione --fail-on warning para falhar também em avisos, --explain (saída
de texto) para explicações de como corrigir cada um. JSON: { summary: { error, warning }, findings: [{ rule, severity, message, file, line }] }.
Se o resultado disser 0 SQL files, as políticas provavelmente existem apenas no dashboard. Peça ao usuário para executar
supabase db pull (ele escreve o schema remoto em uma migration) e escanear novamente. Não reporte o projeto como
limpo a partir de uma varredura vazia.
As verificações
| Regra | Nível | O que significa |
|---|---|---|
rls-disabled | erro | Uma tabela public nunca recebe enable row level security, então qualquer pessoa com a chave anônima pode ler e escrever nela através da API. |
rls-permissive-policy | erro em escrita, aviso em leitura | using (true) ou with check (true). Em insert/update/delete qualquer usuário pode alterar linhas de outros usuários. Em select toda linha é pública. |
rls-no-policy | aviso | RLS ativado, sem políticas: a API nega tudo. Tudo bem para tabelas apenas de servidor; resulta em lista vazia silenciosa se o navegador tentar ler. |
rls-update-privileged-column | aviso | Usuários podem atualizar sua própria linha e ela contém plan, role, is_admin, credits ou similar. O RLS limita linhas, não colunas. |
view-bypasses-rls | erro | Uma view sem security_invoker = true executa como seu dono e ignora o RLS nas tabelas que a compõem. |
auth-users-exposed | erro | Uma view em public seleciona de auth.users. |
definer-search-path | aviso | Uma função security definer sem set search_path. |
definer-function-anon | erro / aviso | Uma função security definer em public que o anônimo pode chamar em /rest/v1/rpc/<nome>. Erro com um grant explícito para anônimo, aviso para o grant padrão do Supabase. |
storage-public-bucket | erro / aviso | Um bucket público. Erro quando uma política de select em storage.objects também permite que qualquer pessoa liste o bucket. |
Como corrigir
Sempre escreva uma nova migração. Nunca edite uma que já foi executada em um banco de dados.
Políticas ausentes ou permissivas: pergunte qual acesso o app precisa, então escopo cada política para um papel e para o dono.
alter table public.notes enable row level security;
create policy "notes: read own" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "notes: insert own" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
Colunas privilegiadas: permita atualizações apenas nas colunas que os usuários podem alterar.
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
Views: alter view public.<nome> set (security_invoker = true); (Postgres 15+, todos os projetos Supabase atuais).
Uma view materializada não pode usar RLS: mova-a para fora de public ou revogue select de anon, authenticated. Para auth.users, copie os campos compartilháveis para public.profiles com um gatilho de cadastro.
Funções SECURITY DEFINER: adicione set search_path = '' e escreva cada nome com seu schema. Em seguida, restrinja
quem pode chamá-la:
revoke execute on function public.admin_set_plan(uuid, text) from public, anon, authenticated;
grant execute on function public.admin_set_plan(uuid, text) to service_role;
Revogar apenas de public não é suficiente no Supabase, porque o anônimo tem seu próprio grant em funções novas.
Armazenamento: torne buckets privados privados, escopo leituras para a pasta do dono
((storage.foldername(name))[1] = (select auth.uid())::text) e sirva URLs assinadas. Imagens de produtos e logotipos
podem permanecer públicos.
Um achado que é intencional (um catálogo público, uma tabela apenas de servidor) pode ser silenciado com
-- vibe-preflight-ignore <id-da-regra> na linha ou na linha acima dela. Explique o motivo no mesmo comentário.
Relatório
- Abra cada linha reportada e confirme-a contra o SQL antes de alterar qualquer coisa. O scanner lê o texto SQL e pode estar errado; diga quando isso acontecer.
- Agrupe o resumo por regra. Erros primeiro, depois os avisos que você confirmou com o usuário.
- Execute a varredura novamente após a correção e reporte o novo resultado.
Limitações
- Apenas o que está em arquivos
.sql. Alterações feitas apenas no dashboard, outros schemas e o que uma função faz dentro de seu corpo não são verificados. - Um resultado limpo significa que esses 9 padrões não foram encontrados. Não prova que as políticas correspondem às regras do aplicativo. Testar como um verdadeiro anônimo e usuário logado (o cliente Supabase com a chave anônima) ainda vale a pena.
- Para o lado do navegador e ambiente (chave service_role em código cliente, segredos em variáveis
NEXT_PUBLIC_,.envcommitado) execute a varredura completa sem--only, ou use a skillvibe-preflight.