Supabase RLS 审计
本技能运行 vibe-preflight 扫描器的数据库部分(scripts/preflight.mjs,位于本文件旁边,Node 20+,无依赖,无网络连接)。它读取项目中的 .sql 文件(Supabase 迁移、schema.sql),按顺序回放表、策略、视图、函数、授权和存储桶定义,并报告 9 类问题。它不连接数据库。
运行
从项目根目录运行:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only database --format json
Claude Code 会自动填充 ${CLAUDE_SKILL_DIR}。对于其他代理:使用本技能的文件夹,例如 .agents/skills/supabase-rls-audit 或 .claude/skills/supabase-rls-audit。
退出码 1 表示至少有一个错误。添加 --fail-on warning 可同时将警告视为失败,--explain(文本输出)可解释每个问题的原因和修复方法。JSON 输出:{ summary: { error, warning }, findings: [{ rule, severity, message, file, line }] }。
如果结果显示 0 SQL files,则策略可能仅存在于仪表盘中。请用户运行 supabase db pull(它会将远程模式写入迁移)并重新扫描。不要因空扫描就报告项目是干净的。
检查项
| 规则 | 级别 | 含义 |
|---|---|---|
rls-disabled | error | public 表从未启用 enable row level security,因此任何拥有 anon 密钥的人都可以通过 API 读写它。 |
rls-permissive-policy | 写入 error,读取 warning | using (true) 或 with check (true)。在插入/更新/删除时,任何用户都可以修改其他用户的行。在查询时,每一行都是公开的。 |
rls-no-policy | warning | RLS 已启用但无策略:API 拒绝一切。对于仅限服务器使用的表来说没问题;但如果浏览器读取,则会收到空列表。 |
rls-update-privileged-column | warning | 用户可以更新自己的行,但该行包含 plan、role、is_admin、credits 等列。RLS 限制行,但不限制列。 |
view-bypasses-rls | error | 没有 security_invoker = true 的视图以其所有者身份运行,并跳过底层表上的 RLS。 |
auth-users-exposed | error | public 模式中的视图查询了 auth.users。 |
definer-search-path | warning | security definer 函数没有 set search_path。 |
definer-function-anon | error / warning | public 模式中的 security definer 函数,且 anon 角色可以通过 /rest/v1/rpc/<name> 调用。显式授权给 anon 时为 error,Supabase 默认授权时为 warning。 |
storage-public-bucket | error / warning | 公开的存储桶。当 storage.objects 上的查询策略也允许任何人列出内容时为 error。 |
如何修复
始终编写新的迁移文件。切勿编辑已在数据库上运行过的迁移。
缺少策略或策略过于宽松:询问应用需要什么访问权限,然后将每个策略限定到特定角色和数据所有者。
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);
特权列:只允许更新用户可更改的列。
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
视图:alter view public.<name> set (security_invoker = true);(需要 Postgres 15+,所有当前 Supabase 项目均已支持)。物化视图无法使用 RLS:将其移出 public 或撤销 anon, authenticated 角色的查询权限。对于 auth.users,使用注册触发器将可共享字段复制到 public.profiles 中。
SECURITY DEFINER 函数:添加 set search_path = '' 并使用模式全名编写每个对象。然后限制谁可以调用它:
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;
在 Supabase 上,仅从 public 撤销是不够的,因为 anon 对新函数有自己的授权。
存储:将私有桶设为私有,将读取范围限定为所有者文件夹((storage.foldername(name))[1] = (select auth.uid())::text),并提供签名 URL。产品图片和 Logo 可以保持公开。
如果某个发现是预期行为(例如公共目录、仅服务器使用的表),可以在该行或其上一行添加 -- vibe-preflight-ignore <rule-id> 并附上原因注释来忽略。
报告
- 在修改任何内容之前,打开每个报告的行并与 SQL 对照确认。扫描器仅读取 SQL 文本,可能出错;如果出错请明确指出。
- 按规则对摘要进行分组。先列出错误,然后是经用户确认的警告。
- 修复后重新运行扫描并报告新结果。
限制
- 仅检查
.sql文件中的内容。仪表盘中的更改、其他模式以及函数体内部的操作不会被检查。 - 结果为干净仅表示未发现这 9 种模式。它并不能证明策略与应用规则匹配。使用真实的 anon 用户和已登录用户(通过 Supabase 客户端和 anon 密钥)进行测试仍然是必要的。
- 对于浏览器和环境端(客户端代码中的 service_role 密钥、
NEXT_PUBLIC_变量中的秘密、已提交的.env),请运行不带--only的完整扫描,或使用vibe-preflight技能。