Lovable + Supabase 上线前安全检查:RLS、Service Role Key 与多租户越权测试

针对 Lovable 生成的 Supabase 应用,逐项检查 RLS、Service Role Key、Storage、Edge Functions 与多租户越权,给出可执行测试。

Lovable 会在发布时运行基础安全扫描,但自动扫描不能证明业务授权正确。

尤其是 Supabase 项目,表上“启用了 RLS”与“策略不会越权”是两件事。

先画清楚身份和数据边界

列出匿名用户、登录用户、管理员和后台任务。

再列出每张表的所有者字段与租户字段。

1
2
3
4
profiles.user_id
projects.owner_id
documents.organization_id
memberships.organization_id + user_id

没有明确归属字段的表,很难写出可靠策略。

确认每张业务表都启用 RLS

在 Supabase SQL Editor 查询真实状态。

1
2
3
4
select schemaname, tablename, rowsecurity
from pg_tables
where schemaname = 'public'
order by tablename;

新增表最容易漏掉 RLS。

把结果保存为上线审计附件。

策略必须分别覆盖四种操作

SELECTINSERTUPDATEDELETE 的风险不同。

允许读取自己的行,不代表应该允许修改所有字段。

插入时使用 WITH CHECK 验证新行归属。

更新时同时检查旧行可见性与新行合法性。

删除操作通常需要更严格角色。

多租户策略不要只比较 user_id

团队产品通常依赖 membership 表。

策略应验证当前用户属于目标 organization。

还要检查成员状态是否有效、角色是否允许操作。

被停用成员不应继续读取旧租户数据。

邀请记录不能等价于正式成员。

用两个账号做越权测试

创建租户 A 用户 Alice 和租户 B 用户 Bob。

让 Alice 创建一条可识别测试记录。

Bob 尝试列表查询、按 ID 查询、更新和删除该记录。

不要只测 UI。

在浏览器开发者工具中复制请求,替换资源 ID 后重放。

所有越权请求应返回空结果或授权错误。

Service Role Key 绝不能进入前端

它会绕过 RLS,必须只存在于受控服务端。

搜索仓库和构建产物:

1
2
rg -n "service_role|SUPABASE_SERVICE" .
rg -n "eyJ[a-zA-Z0-9_-]+\.eyJ" dist build

如果曾经提交过,不是删掉文件就结束。

应立即轮换密钥,并检查 Git 历史与部署日志。

Anon Key 可以公开但权限不能宽松

Supabase anon key 本来用于客户端。

它的安全性依赖 RLS 与数据库权限。

不要因为它可公开,就忽略被滥用后的请求成本。

对高消耗接口增加速率限制与验证码。

Storage 也需要独立策略

检查 bucket 是 public 还是 private。

私有文件使用短期 signed URL。

对象路径最好包含可信的用户或租户前缀。

不要只从客户端文件名推导所有权。

尝试把另一个租户的对象路径传给下载接口。

Edge Function 验证令牌而非相信参数

函数应从 Authorization header 验证用户。

不要接受请求体里的 user_id 作为身份依据。

管理员动作在服务端重新查询角色。

日志中不要打印完整 JWT、Cookie 或支付信息。

数据库函数检查 SECURITY DEFINER

1
2
3
4
5
select n.nspname, p.proname, p.prosecdef
from pg_proc p
join pg_namespace n on n.oid = p.pronamespace
where n.nspname not in ('pg_catalog', 'information_schema')
order by 1, 2;

SECURITY DEFINER 函数以所有者权限运行。

必须固定 search_path,限制 execute 权限,并审查动态 SQL。

没有必要时改回默认的调用者权限。

前端隐藏按钮不是授权

Lovable 生成的页面可能根据角色隐藏管理按钮。

攻击者仍能直接调用 API。

所有权限必须在数据库策略或服务端再次执行。

UI 判断只改善体验,不构成安全边界。

发布前执行负面测试

测试未登录访问。

测试过期 token。

测试普通用户调用管理员接口。

测试跨租户 UUID。

测试批量接口混入一条无权记录。

测试上传超大文件和伪造 MIME 类型。

测试已删除账号的旧 token。

负面用例全部通过,才说明策略不仅覆盖正常路径。

发现问题后的处理顺序

先收紧策略或暂停受影响功能。

再轮换泄露的密钥。

检查审计日志确认是否已被利用。

修复后用两个租户重新测试。

最后才重新发布前端。

不要把安全问题只交给新的自然语言提示“再优化一下”。

最终检查表

  • 所有业务表 RLS 状态已导出。

  • 四类数据库操作都有明确策略。

  • 多租户授权通过 membership 验证。

  • Service Role Key 未进入客户端或 Git。

  • Storage bucket 与对象策略已测试。

  • Edge Function 独立验证身份与角色。

  • SECURITY DEFINER 函数逐个审查。

  • 两账号越权测试覆盖读写删。

  • 密钥轮换与事件响应路径可执行。

自动扫描适合发现常见配置错误;多租户业务规则仍需要人工设计和对抗性测试。

Supabase 安全资料

用 SQL 查看现有策略而不是只看界面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
select
  schemaname,
  tablename,
  policyname,
  roles,
  cmd,
  qual,
  with_check
from pg_policies
where schemaname = 'public'
order by tablename, policyname;

导出结果后逐表核对。qual 决定哪些旧行可见,with_check 决定写入后的新行是否允许存在;只写其中一个,可能造成读写规则不对称。

一个多租户读取策略的审查思路

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
create policy "members can read organization documents"
on public.documents
for select
to authenticated
using (
  exists (
    select 1
    from public.memberships m
    where m.organization_id = documents.organization_id
      and m.user_id = auth.uid()
      and m.status = 'active'
  )
);

真实策略还要结合角色与业务需求。审查时确认 organization_id 有索引,否则每次查询都可能扫描 memberships,安全策略会变成性能瓶颈。

防止用户修改所有权字段

允许用户更新标题时,不应同时允许他把 owner_idorganization_id 改成其他值。可以限制更新列,或者在 WITH CHECK 中要求所有权保持合法。

API 测试应显式发送所有权字段,而不是依赖前端表单不展示它。攻击者可以直接构造 JSON 请求。

邀请流程的竞态条件

团队邀请至少包含随机 token、过期时间、目标邮箱、组织和状态。接受邀请时在一个数据库事务里检查并标记 token 已使用。

同一个邀请链接并发提交两次,只能创建一个 membership。已经撤销或过期的邀请必须失败,不能因为用户已登录就跳过检查。

Storage 上传后进行二次验证

客户端的 Content-Type 可以伪造。服务端或异步任务读取文件头,确认真实格式,并对图片、PDF 等类型设置独立大小限制。

公开 bucket 不放身份证、合同和用户导出文件。私有 bucket 的 signed URL 设置短有效期,日志不记录完整签名 URL。

Edge Function 的 CORS 不是授权

CORS 只控制浏览器能否读取跨域响应,不能阻止 curl 或服务端请求。函数仍需验证 JWT、租户成员关系和具体操作权限。

预检请求只返回必要方法和 Header。不要为了消除浏览器报错而使用任意 origin 加凭据组合。

备份中也包含敏感数据

Supabase 备份、SQL dump 和本地种子文件使用与生产数据相同的保护级别。下载到开发电脑前先确认磁盘加密与访问权限。

恢复演练使用隔离项目。恢复完成后立即轮换测试环境中随数据带入的 token、Webhook secret 与第三方凭据。

上线后的持续检查

监控 RLS 拒绝数量、异常批量读取、Storage 流量和 Edge Function 错误率。单次拒绝通常是正常输入错误,短时间内遍历大量 UUID 则可能是越权探测。

每次新增表、bucket 或函数都重新执行安全清单。不能因为第一次上线审计通过,就认为之后生成的所有功能自动继承正确策略。