Auditoría RLS de Supabase
Este skill ejecuta la parte de base de datos del escáner vibe-preflight (scripts/preflight.mjs junto a este archivo,
Node 20+, sin dependencias, sin red). Lee los archivos .sql del proyecto (migraciones de Supabase,
schema.sql), reproduce tablas, políticas, vistas, funciones, permisos y buckets de almacenamiento en orden, e informa
9 problemas. No se conecta a una base de datos.
Ejecutarlo
Desde la raíz del proyecto:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only database --format json
Claude Code reemplaza ${CLAUDE_SKILL_DIR}. Otros agentes: usar la carpeta de este skill, por ejemplo
.agents/skills/supabase-rls-audit o .claude/skills/supabase-rls-audit.
El código de salida 1 significa al menos un error. Añadir --fail-on warning para que falle también con advertencias, --explain (salida
de texto) para saber por qué y cómo solucionar cada uno. JSON: { summary: { error, warning }, findings: [{ rule, severity, message, file, line }] }.
Si el resultado dice 0 archivos SQL, las políticas probablemente viven solo en el panel de control. Pide al usuario ejecutar
supabase db pull (escribe el esquema remoto en una migración) y escanear de nuevo. No informes el proyecto como limpio
si el escaneo está vacío.
Las comprobaciones
| Regla | Nivel | Significado |
|---|---|---|
rls-disabled | error | Una tabla public nunca recibe enable row level security, por lo que cualquiera con la clave anónima puede leerla y escribirla a través de la API. |
rls-permissive-policy | error en escritura, advertencia en lectura | using (true) o with check (true). En inserción/actualización/eliminación cualquier usuario puede cambiar las filas de otros usuarios. En selección cada fila es pública. |
rls-no-policy | advertencia | RLS activada, sin políticas: la API lo deniega todo. Aceptable para tablas solo de servidor; una lista vacía silenciosa si el navegador lo lee. |
rls-update-privileged-column | advertencia | Los usuarios pueden actualizar su propia fila y esta tiene plan, role, is_admin, credits o similar. RLS limita filas, no columnas. |
view-bypasses-rls | error | Una vista sin security_invoker = true se ejecuta como su propietario y evita RLS en las tablas subyacentes. |
auth-users-exposed | error | Una vista en public selecciona de auth.users. |
definer-search-path | advertencia | Una función security definer sin set search_path. |
definer-function-anon | error / advertencia | Una función security definer en public a la que anon puede llamar en /rest/v1/rpc/<nombre>. Error con un permiso explícito a anon, advertencia para el permiso predeterminado de Supabase. |
storage-public-bucket | error / advertencia | Un bucket público. Error cuando una política de selección en storage.objects también permite que cualquiera lo liste. |
Cómo solucionarlo
Siempre escribe una migración nueva. Nunca edites una que ya se haya ejecutado en una base de datos.
Políticas faltantes o permisivas: preguntar qué acceso necesita la aplicación, luego acotar cada política a un rol y al propietario.
alter table public.notes enable row level security;
create policy "notes: leer propias" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "notes: insertar propias" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
Columnas privilegiadas: permitir actualizaciones solo en las columnas que los usuarios pueden cambiar.
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
Vistas: alter view public.<nombre> set (security_invoker = true); (Postgres 15+, todos los proyectos actuales de Supabase). Una vista materializada no puede usar RLS: muévela fuera de public o revoca select a anon, authenticated. Para auth.users, copia los campos compartibles en public.profiles con un disparador de registro.
Funciones SECURITY DEFINER: añadir set search_path = '' y escribir cada nombre con su esquema. Luego restringir quién puede llamarla:
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;
Revocar solo de public no es suficiente en Supabase, porque anon tiene su propio permiso en funciones nuevas.
Almacenamiento: haz privados los buckets privados, limita las lecturas a la carpeta del propietario
((storage.foldername(name))[1] = (select auth.uid())::text) y sirve URL firmadas. Las imágenes de productos y logotipos
pueden permanecer públicas.
Un hallazgo que es intencional (un catálogo público, una tabla solo de servidor) se puede silenciar con
-- vibe-preflight-ignore <id-de-regla> en la línea o la línea anterior. Escribe el motivo en el mismo comentario.
Informe
- Abre cada línea reportada y confírmala contra el SQL antes de cambiar nada. El escáner lee texto SQL y puede equivocarse; dilo cuando ocurra.
- Agrupa el resumen por regla. Primero los errores, luego las advertencias que confirmaste con el usuario.
- Vuelve a ejecutar el escaneo después de la corrección e informa el nuevo resultado.
Limitaciones
- Solo lo que está en archivos
.sql. Cambios solo en el panel de control, otros esquemas y lo que hace una función dentro de su cuerpo no se comprueban. - Un resultado limpio significa que estos 9 patrones no se encontraron. No prueba que las políticas coincidan con las reglas de la aplicación. Probar como un usuario anónimo real y un usuario autenticado (el cliente de Supabase con la clave anónima) sigue valiendo la pena.
- Para el lado del navegador y entorno (clave service_role en código cliente, secretos en variables
NEXT_PUBLIC_,.envcommiteado) ejecuta el escaneo completo sin--only, o usa el skillvibe-preflight.