文章区分 Supabase 可放在前端的公开密钥与仅限服务端使用的高权限密钥,强调公开密钥的安全性依赖正确的访问控制。它给出检查公共表 RLS 状态的 SQL,并演示启用行级安全和按数据所有者限制访问。
如果你用 Lovable、Bolt、v0 或 Cursor,基于 Supabase 开发过应用,大概有过这样的经历:打开 DevTools,在 JavaScript 里看到一串以 eyJ... 开头的长密钥,心里顿时一紧。
先说个好消息:这个密钥本来就应该出现在那里。下面来看看它到底有什么作用,以及用 AI 开发的 Supabase 应用中,真正会导致数据泄露的四类问题。
Supabase 会为每个项目提供两类密钥:
anon key 只是用来标识你的项目。它本身不授予任何权限:别人拿到它之后能做什么,完全取决于你的 Row Level Security(RLS,行级安全)策略。
但关键也在这里。只有 RLS 真正发挥作用,anon key 才是安全的。
如果没有开启 RLS,任何人只要从你的 JavaScript 中复制出 anon key,就能通过一次请求读取整张表,而且往往还能写入数据。完全不需要登录。
在 SQL editor 中检查每一张表:
select tablename, rowsecurity
from pg_tables
where schemaname = 'public';
凡是显示 false 的表,都处于开放状态。开启 RLS,并添加一条将数据行与其所有者关联起来的策略:
alter table public.profiles enable row level security;
create policy "Users read their own profile"
on public.profiles for select
using (auth.uid() = user_id);
要留意包含 using (true) 的策略。这类策略虽然开启了 RLS,却允许所有人通过。AI 工具有时会生成这样的策略,以“修复”权限错误。
service_role(或 sb_secret_)密钥会完全绕过 RLS。如果它出现在前端,无论你的策略写得多好,每张表都会暴露。
AI 工具通常是在无意中把它放到前端的:为了让某个管理功能正常工作,添加了 VITE_SUPABASE_SERVICE_ROLE_KEY 或 NEXT_PUBLIC_... 这样的变量。任何以 VITE_、NEXT_PUBLIC_ 或 REACT_APP_ 为前缀的变量,都会被打包进每位访客下载的 JavaScript 中。
如果你发现了这种情况:立即在 Supabase 的 Settings → API 中轮换密钥,然后将管理操作移到 Edge Function 中,让它从 Supabase secrets 读取密钥。
Storage 有自己独立的策略,与数据表的 RLS 分开配置,因此很容易被遗忘。一个“公开”的 bucket,意味着任何拿到文件 URL 的人都可以下载文件。头像这样处理没问题,但发票、证件照或导出文件就不适合了。
在 Storage 中检查哪些 bucket 是公开的,并像配置数据表一样,为私有 bucket 在 storage.objects 上设置策略。
视图默认以创建者的权限运行,因此,即使底层数据表受到保护,基于它创建的视图仍可能暴露数据。在 Postgres 15+ 中,创建视图时应使用 with (security_invoker = on)。
标记为 SECURITY DEFINER 的函数也会以所有者的权限运行。如果某个这样的函数可以通过 API 调用,就要检查它是否只返回调用者有权查看的数据。
打开你已上线的应用,然后进入 DevTools → Sources,搜索 service_role、sb_secret_、sk- 和 sk_live。正常情况下,一个都不应该搜到。
运行前面的 SQL。每张表都应该显示 true。
检查你的策略中是否存在 using (true)。
检查哪些 Storage bucket 是公开的。
如果你不想手动检查,我开发了 LeakPatrol。这是一个免费的只读检查工具,可以检查泄露的密钥、公开的 .env 文件,以及缺失的安全响应头。在你证明自己拥有该网站后,它还会检查哪些 Supabase 表可以在未登录的情况下读取。每项检查结果都会附带一个 prompt,你可以把它粘贴回 AI 工具中,让 AI 帮你修复。
anon key 从来都不是问题。真正重要的是,你围绕它构建了什么。
如果还需要采取进一步措施,可以考虑屏蔽此人,或者举报其滥用行为。