// docs / baas security / supabase service role exposure
暴露在 JavaScript 中的 Supabase 服务角色密钥:含义和发现方法
Supabase 服务角色密钥是你数据库的主密钥。任何持有它的人都能绕过行级安全,可以读取每个表的每一列,并且可以随意写入或删除任何内容。它被设计为只存在于服务器端代码中 — 绝不在浏览器中。当 AI 编码工具将其发布到 JavaScript 包时,你的数据库实际上就是公开的。本文解释标识泄露密钥的 JWT 形状、产生泄露的三种 AI 工具模式、检测后第一小时内应采取的行动,以及如何在用户之前自动扫描它。
服务角色密钥是什么
Supabase 为每个项目发行两种不同的密钥:anon 密钥 (在较新项目中也称为可发布密钥) 和 service_role 密钥。两者都是由项目 JWT 密钥签名的 JSON Web Token。区别在于烘焙到 JWT 载荷中的 role 声明 — 公共密钥为 anon,主密钥为 service_role。PostgREST、Supabase Storage 和 Supabase Auth 在看到 service_role 声明时都会切换到绕过一切的模式。
在 jwt.io 解码任何 Supabase 密钥并查看载荷。服务角色 JWT 的形状无可错认:
服务角色 JWT 的解码载荷 (下方以语法高亮块显示)。
{
"iss": "supabase",
"ref": "[project-ref]",
"role": "service_role",
"iat": 1700000000,
"exp": 2000000000
}较新的 Supabase 项目发行带有 sb_secret_ 前缀的机密样式密钥,而不是 JWT。行为相同 — 公共包中任何带有 sb_secret_ 的内容同样具有灾难性。
AI 编码工具如何泄露服务角色密钥
我们在数千个 vibe 编码应用中见过相同的三种模式。每一种都始于开发者请求 AI 工具帮助,结束于服务密钥被内联到包中。
模式 1:带 NEXT_PUBLIC_ 前缀的单个 .env 文件
开发者请求 AI 工具「设置 Supabase」,并接受包含两个密钥的单个 .env。AI 工具 — 在大多数环境变量通过 NEXT_PUBLIC_* 暴露的语料库上训练 — 给两者都加上 NEXT_PUBLIC_ 前缀。Next.js 在构建时将匹配该前缀的任何内容内联到客户端包中。发布到 Vercel,服务密钥就会出现在 main.[hash].js 中。
模式 2:createClient 调用中使用了错误的密钥
开发者将两个密钥粘贴到 AI 生成的 config.ts 文件中,AI 错误地用 process.env.SUPABASE_SERVICE_ROLE_KEY 填充浏览器端的 createClient() 调用。构建拉入变量,JWT 落入包中。
模式 3:种子脚本中硬编码服务角色密钥
开发者请求 AI 工具编写一个为数据库播种的脚本。AI 将服务角色密钥直接硬编码到文件中 (而不是从环境读取),将文件提交到仓库,公共 GitHub 仓库或已部署应用的 /scripts/seed.js 路由现在就在提供该密钥。
FixVibe 包扫描如何检测泄露
FixVibe 的包机密检查会下载已部署应用引用的每个 JavaScript 文件 — 入口块、延迟加载块、Web Worker、Service Worker — 并通过一个检测器运行它们,该检测器会解码任何匹配 JWT 形状 (eyJ[base64-header].eyJ[base64-payload].[signature]) 的内容。如果解码的载荷包含 "role": "service_role",扫描会将其报告为严重发现,附带文件路径和密钥出现的确切行。同一检查还通过前缀匹配较新的 sb_secret_* 模式。
扫描从不使用发现的密钥进行身份验证。它识别形状并报告泄露 — 使用密钥来证明可利用性将构成对你数据库的未授权访问。证据就在 JWT 载荷本身中。
检测到 — 第一小时内该做什么
泄露的服务角色密钥是运行时紧急事件。假设密钥已被抓取 — 攻击者实时监控公共包。在你轮换密钥并审计近期活动之前,将数据库视为已被入侵。
- 立即轮换密钥。在 Supabase 仪表板中,转到 Project Settings → API → Service role key → Reset。旧密钥在几秒内失效。任何使用该密钥的服务端代码必须在轮换生效之前更新并重新部署。
- 审计近期数据库活动。打开仪表板中的 Database → Logs。按最近 7 天筛选。寻找针对含 PII 表的异常
SELECT *查询、大的UPDATE或DELETE语句,以及来自已知基础设施之外 IP 的请求。Supabase 在每个请求上都记录x-real-ip标头。 - 检查存储对象。访问 Storage → Logs 并审查近期的文件下载。泄露的服务角色密钥也给私有桶提供绕过一切的访问。
- 从源代码管理中删除密钥。即使在轮换之后,将 JWT 留在 git 历史中意味着它在公共仓库中仍可被发现。使用
git filter-repo或 BFG Repo-Cleaner 从历史中清除它,然后强制推送 (请先警告协作者)。 - 修复后重新扫描。对重新部署的应用运行新的 FixVibe 扫描。包机密发现应被清除。确认任何块中都没有
service_roleJWT 和sb_secret_*字符串。
从源头预防泄露
结构性修复是命名纪律加工具级护栏:
- 绝不将服务密钥以
NEXT_PUBLIC_*、VITE_*或任何其他包内联前缀作为前缀。命名约定就是边界 — 每个框架都尊重它。 - 完全从开发者机器的
.env中移除服务密钥。在部署时从机密管理器 (Doppler、Infisical、Vercel 加密环境变量) 读取它,绝不在本地提交。 - <strong>Mark every Supabase client construction with explicit context.</strong> Files named <code>supabase/browser.ts</code> use the anon key; files named <code>supabase/server.ts</code> use the service-role key with <code>import 'server-only'</code> at the top. The <code>server-only</code> import causes a build error if a client component tries to consume the module.
- <strong>Add a pre-commit hook that greps for JWT-shaped strings.</strong> <code>git diff --staged | grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+'</code> catches both anon and service tokens before they leave your machine.
- 添加扫描构建输出的 CI 门禁。在
next build之后,grep.next/static/chunks/输出中的service_role字符串。如果有任何匹配,构建失败。
# Pre-commit hook: refuse any staged JWT-shaped string.
git diff --staged \
| grep -E 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' \
&& echo "JWT detected in staged changes — refusing commit" \
&& exit 1
# CI gate: fail the build if "service_role" shipped to the static bundle.
grep -RE 'service_role|sb_secret_' .next/static/chunks/ \
&& echo "Service-role credential leaked into bundle" \
&& exit 1常见问题
攻击者实际上能多快找到泄露的 Supabase 服务角色密钥?
公共包扫描器会在几分钟内扫描新部署。研究人员记录过对新 Supabase 项目从首次部署到 1 小时内的有效漏洞利用。将任何服务角色暴露视为 60 分钟窗口,而非 60 天窗口。
轮换密钥就够了,还是必须假设数据已被外泄?
轮换会使泄露的密钥失效,但不会撤销已被拉取的数据。如果你的表包含 PII、支付数据或任何受监管的数据,你可能在 GDPR (72 小时)、CCPA 或 HIPAA 下有通知义务。审计日志,如果审计显示可疑访问,请咨询法律顾问。
如果服务角色密钥泄露,RLS 能保护我吗?
不能。行级安全完全被 service_role 声明绕过。这是设计如此 — 该密钥的存在正是为了让后端代码跳过 RLS 以进行管理操作。缓解措施是确保密钥永远不到达攻击者可以读取的上下文。
这适用于新的 Supabase 可发布 / 机密密钥模型 (<code>sb_publishable_</code> / <code>sb_secret_</code>) 吗?
是的 — 同等风险等级。sb_secret_* 密钥是较新项目中取代服务角色 JWT 的新机密密钥格式。包中带有 sb_secret_* 的任何内容与泄露的服务角色 JWT 同样具有灾难性。FixVibe 的包机密检测器匹配两种形状。
anon / 可发布密钥呢 — 它在包中安全吗?
是的,按设计是安全的。anon 密钥旨在存在于浏览器中,是每个 Supabase Web 客户端使用的密钥。它的安全性完全取决于每个公共表上 RLS 是否正确配置。请参阅 Supabase RLS 扫描器 文章了解需要检查什么。
后续步骤
对你的生产 URL 运行 FixVibe 扫描 — 包机密检查免费、无需注册,并在一分钟内报告 service_role 暴露。将此与 Supabase RLS 扫描器 文章配合,以验证 RLS 层正在发挥作用,并配合 Supabase 存储桶安全清单 锁定文件访问。有关 AI 工具为何如此可靠地生成此类泄露的背景,请阅读 AI 编码工具为何留下安全漏洞。
