// docs / baas security / supabase rls scanner
Supabase RLS 扫描器:发现缺失或损坏的行级安全表
当你部署一个由 Supabase 支持的应用时,行级安全 (RLS) 是站在你的客户数据和互联网之间的唯一屏障。AI 编码工具生成可以编译、发布并悄悄泄露数据的 RLS 形状代码 — 创建时未启用 RLS 的表、可读但从不限制的策略、将列与自身比较的谓词。本文展示 Supabase RLS 扫描器从外部可以证明什么、出现在 vibe 编码应用中的四种损坏 RLS 形态,以及如何在不到一分钟内扫描你自己的部署。
外部 RLS 扫描可以证明什么
被动 RLS 扫描针对 Supabase 在 https://[project].supabase.co/rest/v1/ 暴露的 PostgREST 端点运行。它只使用可发布的 anon 密钥 — 与你浏览器使用的相同密钥 — 并探测表列表元数据、匿名读取和匿名写入。它从不以用户身份进行身份验证,也从不接触服务角色权限。它能做的任何事,互联网上的未认证攻击者都能做。
从数据库外部,扫描器可以高置信度地确认以下内容:
- 表上 RLS 被禁用。 当 RLS 关闭或策略允许时,PostgREST 会为匿名
SELECT返回行。任一情况都是一项发现。 - 匿名角色可以列出表。 使用 anon 密钥的
GET /rest/v1/会返回anon角色拥有任何权限的每个表的 OpenAPI 模式。AI 生成的应用经常授予模式的USAGE和每个表的SELECT,即使 RLS 拒绝实际读取,也会暴露完整的模式图。 - 匿名角色可以插入。 如果 RLS 没有拒绝
INSERT的策略,猜测列形状的探测性POST将成功 — 即使SELECT已被锁定。 - 服务角色密钥在浏览器包中。 与 RLS 相邻:如果扫描器在 JavaScript 包中发现
SUPABASE_SERVICE_ROLE_KEY或任何带有role: service_role的 JWT,RLS 就形同虚设 — 该密钥持有者可绕过所有策略。
外部扫描不能证明什么
请诚实面对扫描器的边界。外部 RLS 扫描无法读取你的 pg_policies 表、迁移文件或任何策略的确切谓词。它从黑盒行为推断,这意味着它有时会报告一个最终被证明是有意公开数据 (营销新闻通讯表、公共产品目录) 的 发现。当扫描器无法消除意图歧义时,FixVibe 报告会将其标记为中等置信度 — 请审查表名并自行判断。
AI 工具产生的四种损坏 RLS 形态
当你将 Cursor、Claude Code、Lovable 或 Bolt 指向 Supabase 时,数千个应用中会出现相同的四种损坏 RLS 模式。每一种都通过类型检查、编译并发布:
形态 1:从未启用 RLS
最常见的失败模式。迁移创建了表,但开发者 (或 AI 工具) 忘了 ALTER TABLE ... ENABLE ROW LEVEL SECURITY。PostgREST 愉快地将整张表提供给任何拥有 anon 密钥的人。修复: ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY; ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;。FORCE 是必需的 — 没有它,表所有者 (以及任何拥有表所有权的角色) 会绕过 RLS。
ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;形态 2:启用了 RLS,无策略
一种更微妙的失败。RLS 已启用,但没有编写策略。PostgreSQL 的默认是拒绝,所以已认证用户什么也看不到 — 于是开发者添加 USING (true) 以让应用工作,这允许所有人读取所有内容。修复:编写按 auth.uid() 限定范围的策略:CREATE POLICY "select_own" ON public.[name] FOR SELECT USING (auth.uid() = user_id); 以及匹配的 INSERT/UPDATE/DELETE 策略。
CREATE POLICY "select_own"
ON public.[name]
FOR SELECT
USING (auth.uid() = user_id);形态 3:策略将列与自身比较
A copy-paste artefact. The developer writes <code>USING (user_id = user_id)</code> — which is always true — instead of <code>USING (auth.uid() = user_id)</code>. Type-checks pass; the policy permits every row. <strong>Fix:</strong> always compare a column to a function call (<code>auth.uid()</code>, <code>auth.jwt()->>'org_id'</code>, etc.), never to itself or to a constant.
形态 4:SELECT 上有策略但 INSERT/UPDATE 上没有
开发者锁定了读取,但忘记了写入。RLS 策略是按命令的。FOR SELECT 只保护读取;如果没有策略拒绝,匿名客户端仍可 INSERT。修复:为每个命令编写策略,或使用带有明确 USING 和 WITH CHECK 子句的 FOR ALL。
FixVibe Supabase RLS 扫描器的工作原理
baas.supabase-rls 检查分三个阶段运行,每个阶段都有明确的置信度级别:
- 阶段 1 — 指纹识别。扫描器抓取已部署的应用,解析其 JavaScript 包,并从运行时配置中提取 Supabase 项目 URL 和 anon 密钥。无 DNS 猜测,无暴力破解 — 它读取浏览器读取的内容。
- 阶段 2 — 模式发现。使用 anon 密钥的单次
GET /rest/v1/返回 anon 角色能看到的每个表的 OpenAPI 模式。此阶段扫描器记录表名,但不读取行数据。 - 阶段 3 — 读写探测。对每个发现的表,扫描器发出一次带有
limit=1的匿名SELECT。如果返回行,则 RLS 是允许的。扫描器就此停止 — 不枚举行,不分页,不修改数据。INSERT 探测被验证的域名所有权和明确的选择加入所限制;它们从不针对未验证的目标触发。
每项发现都附带确切的请求 URL、响应状态、响应形状 (仅标头) 和表名。发现底部的 AI 修复提示是一个可复制粘贴的 SQL 块,你可以在 Supabase SQL 编辑器中运行。
扫描器发现问题时该怎么做
每一项 RLS 发现都是运行时紧急事件。公共 PostgREST 端点会在几分钟内被攻击者扫描。修复顺序是机械化的:
- 审计每张表。在 Supabase SQL 编辑器中运行
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';。任何rowsecurity = false的行都是问题。 - 在每个公共表上启用 RLS。对创建的每个表默认使用
ENABLE ROW LEVEL SECURITY和FORCE ROW LEVEL SECURITY— 使其成为迁移模板。 - 逐命令编写策略。不要使用
FOR ALL USING (true)。为 SELECT、INSERT、UPDATE、DELETE 各写明确策略 — 每个都按auth.uid()或来自auth.jwt()的组织 ID 列限定范围。 - 用第二个账户验证。注册为不同用户,尝试通过 REST API 直接读取另一用户的记录。如果响应是
200,则策略已损坏。 - 重新扫描。应用修复后,对同一 URL 重新运行 FixVibe 扫描。
baas.supabase-rls发现应被清除。
-- Audit every table for missing RLS. Run in the Supabase SQL editor.
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;与其他扫描器的比较
大多数通用 DAST 工具 (Burp Suite、OWASP ZAP、Nessus) 不知道 PostgREST 是什么。它们会抓取你的应用,忽略 /rest/v1/ 路径,并对它们理解的 HTML 页面进行报告。Snyk 和 Semgrep 是静态分析工具 — 它们能在你的仓库中找到缺少 RLS 调用的迁移文件,但无法证明已部署的数据库配置错误。FixVibe 处于这个空隙:被动、BaaS 感知、专注于未认证攻击者从公共 URL 可证明的内容。
常见问题
扫描器会读取或修改我的数据吗?
不会。被动扫描对每个发现的表最多发出一次 SELECT ... limit=1,以确认 RLS 是否允许匿名读取。扫描器记录响应形状,而非行内容。INSERT、UPDATE 和 DELETE 探测被验证的域名所有权所限制,并且永远不针对未验证的目标运行。
如果我的 Supabase 项目已暂停或使用自定义域名,这个仍能工作吗?
已暂停的项目对每个请求返回 503 — 扫描器报告项目不可达。只要已部署的应用仍在浏览器中加载 Supabase 客户端 SDK,自定义域名就能工作;无论哪种方式,扫描器都从包中提取项目 URL。
如果我的 anon 密钥被轮换或可发布密钥更改了怎么办?
重新运行扫描。扫描器在每次运行时都从当前包重新提取密钥。轮换只会使先前的报告失效,而不会影响数据库的策略状态。
扫描器会检查新的 Supabase 可发布密钥模型 (<code>sb_publishable_*</code>) 吗?
会。检测器识别旧的 anon JWT 和较新的 sb_publishable_* 密钥,并将它们视为相同 — 两者都旨在公开,且都将 RLS 作为唯一的防线。
后续步骤
对你的生产 URL 运行免费的 FixVibe 扫描 — baas.supabase-rls 检查在每个套餐 (包括免费层) 上都已启用。如需深入了解 Supabase 项目还可能泄露什么,请参阅 暴露在 JavaScript 中的 Supabase 服务角色密钥 和 Supabase 存储桶安全清单。对于所有 BaaS 提供商的总览,请阅读 BaaS 配置错误扫描器。
