名称: supabase-extract-service-key 描述: 关键 - 检测 Supabase 服务角色密钥是否在客户端代码中泄露。这是一个 P0 严重性问题。
Supabase 服务密钥检测
🔴 关键: 渐进式文件更新要求
您必须在进行过程中写入上下文文件,而不是只在结束时。
- 每次发现后立即写入
.sb-pentest-context.json- 每次操作前后记录到
.sb-pentest-audit.log- 不要等到技能完成才更新文件
- 如果技能崩溃或中断,所有先前发现必须已经保存
这不是可选的。不渐进式写入是严重错误。
这个技能检测 服务角色密钥(管理员密钥)是否意外暴露在客户端代码中。
何时使用此技能
- 作为每次安全审计的一部分(这是关键的)
- 在部署到生产环境前审查代码时
- 检测到 Supabase 使用后检查此常见错误
先决条件
- 目标应用程序可访问
- Supabase 检测已完成(如果需要则自动调用)
为什么这是关键的
服务角色密钥 绕过所有行级安全(RLS)策略。如果暴露:
| 影响 | 描述 |
|---|---|
| 🔴 完全数据库访问 | 读取/写入/删除所有表中的所有数据 |
| 🔴 身份验证绕过 | 无需身份验证访问所有用户数据 |
| 🔴 存储访问 | 读取/写入所有存储桶中的所有文件 |
| 🔴 用户冒充 | 为任何用户生成令牌 |
这是一个 P0(关键)发现,需要立即行动。
服务密钥与匿名密钥
| 方面 | 匿名密钥 | 服务密钥 |
|---|---|---|
| 角色声明 | "role": "anon" |
"role": "service_role" |
| RLS | ✅ 尊重 RLS | ❌ 绕过 RLS |
| 客户端 | ✅ 预期 | ❌ 从不 |
| 服务器端 | ✅ 可以使用 | ✅ 应该使用 |
检测模式
技能搜索:
1. 具有 service_role 声明的密钥
// 解码的 JWT 负载包含:
{
"role": "service_role", // ❌ 如果在客户端代码中则关键
"iss": "supabase",
"ref": "abc123def"
}
2. 变量名称
// 常见命名模式
SUPABASE_SERVICE_KEY
SUPABASE_SERVICE_ROLE_KEY
SUPABASE_ADMIN_KEY
SUPABASE_SECRET_KEY
SERVICE_ROLE_KEY
3. 意外暴露
// 有时与匿名密钥一起暴露
const keys = {
anon: 'eyJ...',
service: 'eyJ...' // ❌ 不应该在这里
}
使用方式
基本检查
在 https://myapp.example.com 上检查服务密钥泄露
深度扫描
在 https://myapp.example.com 上深度扫描服务密钥暴露
输出格式
未找到服务密钥(好)
═══════════════════════════════════════════════════════════
服务密钥检查
═══════════════════════════════════════════════════════════
状态: ✅ 在客户端代码中未检测到服务角色密钥
扫描内容:
├── HTML 源: 干净
├── JavaScript 包: 5 个文件,2.3MB 分析
├── 内联脚本: 12 个块检查
└── 源映射: 未暴露(好)
JWT 分析:
└── 找到 1 个密钥,确认角色=匿名(安全)
结果: 通过 - 无关键密钥暴露
═══════════════════════════════════════════════════════════
找到服务密钥(关键)
═══════════════════════════════════════════════════════════
🔴 关键: 服务密钥暴露
═══════════════════════════════════════════════════════════
严重性: P0 - 关键
状态: ❌ 在客户端代码中找到服务角色密钥!
⚠️ 需要立即行动 ⚠️
暴露的密钥:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBh
YmFzZSIsInJlZiI6ImFiYzEyM2RlZiIsInJvbGUiOiJzZXJ2aWNlX3
JvbGUiLCJpYXQiOjE2NDAwMDAwMDAsImV4cCI6MTk1NTM2MDAwMH0
.xxxxxxxxxxxxx
位置:
└── /static/js/admin.chunk.js (第 89 行)
const SUPABASE_KEY = 'eyJhbG...' // 在 createClient() 中使用
解码的负载:
├── 角色: service_role ← 关键
├── 引用: abc123def
└── 过期时间: 2031-12-20
影响评估:
├── 🔴 可能完全数据库访问
├── 🔴 所有 RLS 策略被绕过
├── 🔴 所有用户数据暴露
└── 🔴 所有存储桶可访问
═══════════════════════════════════════════════════════════
立即修复步骤
═══════════════════════════════════════════════════════════
1. 立即旋转密钥
→ Supabase 仪表板 > 设置 > API > 重新生成服务角色密钥
2. 从客户端代码中删除
→ 从源代码中删除密钥
→ 重新部署应用程序
3. 审计滥用
→ 检查 Supabase 日志以查找未经授权的访问
→ 审查数据库以查找意外更改
4. 使用边缘函数
→ 将特权操作移到边缘函数
→ 客户端调用边缘函数,边缘函数在服务器端使用服务密钥
文档:
→ https://supabase.com/docs/guides/api/api-keys
→ https://supabase.com/docs/guides/functions
═══════════════════════════════════════════════════════════
上下文输出
保存到 .sb-pentest-context.json:
{
"findings": [
{
"id": "SERVICE_KEY_EXPOSED",
"severity": "P0",
"title": "服务角色密钥在客户端代码中暴露",
"description": "服务角色密钥在客户端 JavaScript 中找到",
"location": {
"file": "/static/js/admin.chunk.js",
"line": 89
},
"evidence": {
"key_prefix": "eyJhbGciOiJIUzI1NiI...",
"role": "service_role",
"project_ref": "abc123def"
},
"remediation": {
"immediate": "在 Supabase 仪表板中旋转密钥",
"long_term": "移到边缘函数",
"docs": "https://supabase.com/docs/guides/api/api-keys"
}
}
],
"supabase": {
"service_key_exposed": true,
"service_key_location": "/static/js/admin.chunk.js:89"
}
}
源映射检查
技能还检查可能暴露密钥的源映射:
源映射分析:
├── main.js.map: ❌ 暴露(可能包含秘密)
├── vendor.js.map: ❌ 暴露
└── 推荐: 在生产环境中禁用源映射
检查源映射内容:
→ 添加 .map 到 JS URL:/static/js/main.js.map
常见原因
| 原因 | 解决方案 |
|---|---|
| 错误的环境变量 | 仅对匿名密钥使用 NEXT_PUBLIC_ |
| 复制粘贴错误 | 仔细检查使用的密钥 |
| 调试代码遗留 | 生产构建前移除 |
| 错误配置的打包器 | 确保服务密钥环境变量未包含 |
修复代码示例
之前(错误)
// ❌ 错误 - 服务密钥在客户端
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_KEY // ❌ 永远不要这样做
)
之后(正确)
// ✅ 正确 - 仅匿名密钥在客户端
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY // ✅ 对客户端安全
)
// 对于特权操作,调用边缘函数:
const { data } = await supabase.functions.invoke('admin-action', {
body: { action: 'delete-user', userId: '123' }
})
边缘函数(服务器端)
// supabase/functions/admin-action/index.ts
import { createClient } from '@supabase/supabase-js'
Deno.serve(async (req) => {
// ✅ 服务密钥仅在服务器
const supabase = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') // ✅ 在服务器安全
)
// 执行特权操作
// ...
})
强制性: 渐进式上下文文件更新
⚠️ 此技能必须在执行过程中渐进式更新跟踪文件,而不是只在结束时。
关键规则: 渐进写入
不要在结束时批量所有写入。而是:
- 在开始任何操作之前 → 将操作记录到
.sb-pentest-audit.log - 每次发现后 → 立即更新
.sb-pentest-context.json - 每个重要步骤后 → 将完成记录到
.sb-pentest-audit.log
这确保如果技能被中断、崩溃或超时,所有到该点的发现都被保留。
必需操作(渐进式)
-
用发现更新
.sb-pentest-context.json:{ "supabase": { "service_key_exposed": true/false, "service_key_location": "路径:行" }, "findings": [ { "id": "SERVICE_KEY_EXPOSED", "severity": "P0", "title": "服务角色密钥暴露", ... } ] } -
记录到
.sb-pentest-audit.log:[时间戳] [supabase-extract-service-key] [开始] 检查服务密钥暴露 [时间戳] [supabase-extract-service-key] [关键] 服务密钥暴露在 路径:行 [时间戳] [supabase-extract-service-key] [上下文已更新] .sb-pentest-context.json 更新 -
如果文件不存在,在写入前创建。
不更新上下文文件是不可接受的。
强制性: 证据收集
📁 证据目录: .sb-pentest-evidence/02-extraction/service-key-exposure/
要创建的证据文件(如果找到服务密钥)
| 文件 | 内容 |
|---|---|
service-key-exposure/location.txt |
文件路径和行号 |
service-key-exposure/decoded-payload.json |
解码的 JWT 证明它是 service_role |
service-key-exposure/code-snippet.txt |
代码上下文(已编辑) |
证据格式(P0 发现)
{
"evidence_id": "EXT-SVC-001",
"timestamp": "2025-01-31T10:10:00Z",
"category": "extraction",
"type": "service_key_exposure",
"severity": "P0",
"finding_id": "P0-001",
"key_data": {
"key_prefix": "eyJhbGciOiJIUzI1NiI...",
"key_suffix": "...xxxx",
"role": "service_role"
},
"decoded_payload": {
"iss": "supabase",
"ref": "abc123def",
"role": "service_role",
"iat": "2021-12-20T00:00:00Z",
"exp": "2031-12-20T00:00:00Z"
},
"location": {
"file": "/static/js/admin.chunk.js",
"line": 89,
"context": "const SUPABASE_KEY = 'eyJhbG...' // [已编辑]"
},
"impact": {
"rls_bypass": true,
"full_db_access": true,
"auth_users_access": true,
"storage_access": true
},
"curl_command": "curl -X GET 'https://abc123def.supabase.co/rest/v1/users' -H 'apikey: [SERVICE_KEY]' -H 'Authorization: Bearer [SERVICE_KEY]'"
}
添加到 timeline.md(P0)
## [时间戳] - 🔴 P0 关键: 服务角色密钥暴露
- 服务角色密钥在客户端代码中找到
- 位置: [文件]:[行]
- 影响: 完全数据库访问,RLS 绕过
- 证据: `02-extraction/service-key-exposure/`
- **需要立即行动**
相关技能
supabase-extract-anon-key— 提取(预期的)匿名密钥supabase-audit-tables-read— 测试可访问的数据supabase-report— 生成包括此发现的完整报告