关于 supabase-audit-auth-users
用于 Supabase 认证安全审计的技能,通过检查注册、登录、恢复和 OTP 流程,识别用户枚举及响应时间泄漏,并逐步保存审计结果。
主要功能
- 01检查注册、登录、恢复和 OTP 端点
- 02比较错误信息、状态码与响应时间
- 03渐进更新上下文和审计日志
- 04保存测试证据并提供缓解建议
适用场景
- 01上线前检查认证流程是否泄露用户存在信息
- 02审计密码恢复和 OTP 请求的响应差异
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yoanbernabeu/ supabase-pentest-skills --skill supabase-audit-auth-users安装后在 Claude Code 中输入/supabase-audit-auth-users确认已加载。
详细介绍
用户枚举审计
🔴 严重:必须渐进式更新文件
你必须在操作过程中写入上下文文件,而不是等到最后才写。
- 每测试完一个端点,立即写入
.sb-pentest-context.json- 每次测试前后都要记录到
.sb-pentest-audit.log- 不要等到该技能执行完毕后才更新文件
- 如果该技能崩溃或被中断,必须已保存之前的所有发现
这不是可选项。不进行渐进式写入属于严重错误。
此技能用于测试身份验证流程中的用户枚举漏洞。
何时使用此技能
- 检查是否可以检测到用户是否存在
- 测试登录、注册和账户恢复流程是否泄露信息
- 作为身份验证安全审计的一部分
- 在部署到生产环境之前
前提条件
- 已获取 Supabase URL 和 anon key
- 身份验证端点可访问
什么是用户枚举?
当应用通过以下方式泄露用户账户是否存在时,就会发生用户枚举:
| 途径 | 指示信号 |
|---|---|
| 不同的错误消息 | “未找到用户”与“密码错误” |
| 响应时间 | 不存在的账户响应快,已存在的账户响应慢 |
| 响应代码 | 404 与 401 |
| 注册响应 | “电子邮件已注册” |
重要性
| 风险 | 影响 |
|---|---|
| 定向攻击 | 攻击者知道哪些账户有效 |
| 网络钓鱼 | 确认目标是否拥有账户 |
| 凭证填充攻击 | 缩小攻击范围 |
| 隐私 | 泄露用户是否存在 |
执行的测试
| 端点 | 测试方法 |
|---|---|
/auth/v1/signup | 尝试注册已存在的电子邮件 |
/auth/v1/token | 尝试使用各种电子邮件登录 |
/auth/v1/recover | 尝试重置密码 |
/auth/v1/otp | 尝试为各种电子邮件获取 OTP |
用法
基本枚举测试
Test for user enumeration vulnerabilities
测试特定端点
Test login endpoint for user enumeration
输出格式
═══════════════════════════════════════════════════════════
USER ENUMERATION AUDIT
═══════════════════════════════════════════════════════════
Project: abc123def.supabase.co
─────────────────────────────────────────────────────────
Signup Endpoint (/auth/v1/signup)
─────────────────────────────────────────────────────────
Test: POST with known existing email
Response for existing: "User already registered"
Response for new email: User object returned
Status: 🟠 P2 - ENUMERABLE
The response clearly indicates if an email is registered.
Exploitation:
```bash
curl -X POST https://abc123def.supabase.co/auth/v1/signup \
-H "apikey: [anon-key]" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "test123"}'
# If user exists: {"msg": "User already registered"}
# If new user: User created or confirmation needed
───────────────────────────────────────────────────────── 登录端点(/auth/v1/token) ─────────────────────────────────────────────────────────
测试:使用不同的电子邮件场景发送 POST 请求
电子邮件已存在,密码错误: ├── 响应:{"error": "Invalid login credentials"} ├── 耗时:245ms └── 状态码:400
电子邮件不存在: ├── 响应:{"error": "Invalid login credentials"} ├── 耗时:52ms ← 快得多! └── 状态码:400
状态:🟠 P2 - 可通过计时枚举
虽然错误消息相同,但响应时间有明显差异: ├── 现有用户:~200-300ms(密码哈希处理) └── 不存在的用户:~50-100ms(不进行哈希检查)
计时攻击概念验证:
import requests
import time
def check_user(email):
start = time.time()
requests.post(
'https://abc123def.supabase.co/auth/v1/token',
params={'grant_type': 'password'},
json={'email': email, 'password': 'wrong'},
headers={'apikey': '[anon-key]'}
)
elapsed = time.time() - start
return elapsed > 0.15 # Threshold
exists = check_user('[email protected]')
───────────────────────────────────────────────────────── 密码恢复(/auth/v1/recover) ─────────────────────────────────────────────────────────
测试:使用不同的电子邮件发送 POST 恢复请求
电子邮件已存在: ├── 响应:{"message": "Password recovery email sent"} ├── 耗时:1250ms(实际发送了电子邮件) └── 状态码:200
电子邮件不存在: ├── 响应:{"message": "Password recovery email sent"} ├── 耗时:85ms ← 快得多(未发送电子邮件) └── 状态码:200
状态:🟠 P2 - 可通过计时枚举
消息相同,但计时信息会暴露用户是否存在。 现有用户会触发实际的电子邮件发送(约 1 秒以上)。
───────────────────────────────────────────────────────── Magic Link / OTP(/auth/v1/otp) ─────────────────────────────────────────────────────────
测试:为不同的电子邮件请求 OTP
电子邮件已存在: ├── 响应:{"message": "OTP sent"} ├── 耗时:1180ms └── 状态码:200
电子邮件不存在: ├── 响应:{"error": "User not found"} ├── 耗时:95ms └── 状态码:400
状态:🔴 P1 - 可直接枚举
错误消息明确说明用户不存在。
───────────────────────────────────────────────────────── 总结 ─────────────────────────────────────────────────────────
已测试的端点:4 可枚举:4(100%)
漏洞严重程度: ├── 🔴 P1:OTP 端点(消息明确) ├── 🟠 P2:注册端点(消息明确) ├── 🟠 P2:登录端点(计时攻击) └── 🟠 P2:恢复端点(计时攻击)
整体用户枚举风险:高
攻击者可以判断任何电子邮件地址 是否在你的应用中拥有账户。
───────────────────────────────────────────────────────── 缓解建议 ─────────────────────────────────────────────────────────
-
统一响应 在所有场景中返回相同的消息: “如果账户存在,你将收到一封电子邮件”
-
统一耗时 添加人为延迟,使响应时间趋于一致:
const MIN_RESPONSE_TIME = 1000; // 1 second const start = Date.now(); // ... perform auth operation ... const elapsed = Date.now() - start; await new Promise(r => setTimeout(r, Math.max(0, MIN_RESPONSE_TIME - elapsed) )); return response; -
速率限制 已启用:每个 IP 每小时 3 次 也可考虑按电子邮件进行速率限制。
-
CAPTCHA 对重复尝试添加 CAPTCHA:
- 3 次登录失败后
- 密码恢复时
- 注册时
-
监控 对枚举模式发出警报:
- 使用不同电子邮件发起大量请求
- 连续的电子邮件模式(user1@、user2@……)
═══════════════════════════════════════════════════════════
## Timing Analysis
The skill measures response times to detect timing-based enumeration:
现有用户: ├── 密码哈希验证:约 200-300ms ├── 发送电子邮件:约 1000-2000ms └── 数据库查询:约 5-20ms
不存在的用户: ├── 无哈希验证:0ms ├── 不发送电子邮件:0ms └── 数据库查询:约 5-20ms(未找到)
Threshold detection:
- Difference > 100ms: Possible timing leak
- Difference > 500ms: Definite timing leak
## Context Output
```json
{
"user_enumeration": {
"timestamp": "2025-01-31T13:30:00Z",
"endpoints_tested": 4,
"vulnerabilities": [
{
"endpoint": "/auth/v1/otp",
"severity": "P1",
"type": "explicit_message",
"existing_response": "OTP sent",
"missing_response": "User not found"
},
{
"endpoint": "/auth/v1/signup",
"severity": "P2",
"type": "explicit_message",
"existing_response": "User already registered",
"missing_response": "User created"
},
{
"endpoint": "/auth/v1/token",
"severity": "P2",
"type": "timing_attack",
"existing_time_ms": 245,
"missing_time_ms": 52
},
{
"endpoint": "/auth/v1/recover",
"severity": "P2",
"type": "timing_attack",
"existing_time_ms": 1250,
"missing_time_ms": 85
}
]
}
}
缓解措施代码示例
统一响应时间
// Edge Function with normalized timing
const MIN_RESPONSE_TIME = 1500; // 1.5 seconds
Deno.serve(async (req) => {
const start = Date.now();
try {
// Perform actual auth operation
const result = await handleAuth(req);
// Normalize response time
const elapsed = Date.now() - start;
await new Promise(r => setTimeout(r,
Math.max(0, MIN_RESPONSE_TIME - elapsed)
));
return new Response(JSON.stringify(result));
} catch (error) {
// Same timing for errors
const elapsed = Date.now() - start;
await new Promise(r => setTimeout(r,
Math.max(0, MIN_RESPONSE_TIME - elapsed)
));
// Generic error message
return new Response(JSON.stringify({
message: "Check your email if you have an account"
}));
}
});
通用错误消息
// Don't reveal user existence
async function requestPasswordReset(email: string) {
// Always return success message
const response = {
message: "If an account with that email exists, " +
"you will receive a password reset link."
};
// Perform actual reset in background (don't await)
supabase.auth.resetPasswordForEmail(email).catch(() => {});
return response;
}
强制要求:渐进式更新上下文文件
⚠️ 此技能必须在执行过程中逐步更新跟踪文件,而不是只在最后更新。
关键规则:边做边写
不要等到最后才批量写入。请按以下步骤操作:
- 测试每个端点之前 → 将操作记录到
.sb-pentest-audit.log - 每次测量时间之后 → 立即更新
.sb-pentest-context.json - 每发现一个枚举向量之后 → 立即记录发现
这样可以确保即使技能执行中断、崩溃或超时,截至当时的所有发现都能得到保留。
必须执行的操作(逐步进行)
-
更新
.sb-pentest-context.json,写入结果:{ "user_enumeration": { "timestamp": "...", "endpoints_tested": 4, "vulnerabilities": [ ... ] } } -
记录到
.sb-pentest-audit.log:[TIMESTAMP] [supabase-audit-auth-users] [START] Testing user enumeration [TIMESTAMP] [supabase-audit-auth-users] [FINDING] P1: OTP endpoint enumerable [TIMESTAMP] [supabase-audit-auth-users] [CONTEXT_UPDATED] .sb-pentest-context.json updated -
如果文件不存在,请在写入前创建。
不更新上下文文件是不可接受的。
必须:收集证据
📁 证据目录: .sb-pentest-evidence/05-auth-audit/enumeration-tests/
需要创建的证据文件
| 文件 | 内容 |
|---|---|
enumeration-tests/login-timing.json | 登录端点时间分析 |
enumeration-tests/recovery-timing.json | 恢复端点时间分析 |
enumeration-tests/otp-enumeration.json | OTP 端点消息分析 |
证据格式
{
"evidence_id": "AUTH-ENUM-001",
"timestamp": "2025-01-31T11:00:00Z",
"category": "auth-audit",
"type": "user_enumeration",
"tests": [
{
"endpoint": "/auth/v1/token",
"test_type": "timing_attack",
"severity": "P2",
"existing_user_test": {
"email": "[KNOWN_EXISTING]@example.com",
"response_time_ms": 245,
"response": {"error": "Invalid login credentials"}
},
"nonexisting_user_test": {
"email": "[email protected]",
"response_time_ms": 52,
"response": {"error": "Invalid login credentials"}
},
"timing_difference_ms": 193,
"result": "ENUMERABLE",
"impact": "Can determine if email has account via timing"
},
{
"endpoint": "/auth/v1/otp",
"test_type": "explicit_message",
"severity": "P1",
"existing_user_response": {"message": "OTP sent"},
"nonexisting_user_response": {"error": "User not found"},
"result": "ENUMERABLE",
"impact": "Error message explicitly reveals user existence"
}
],
"curl_commands": [
"# Timing test - existing user\ntime curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"[email protected]\", \"password\": \"wrong\"}'",
"# Timing test - non-existing user\ntime curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"[email protected]\", \"password\": \"wrong\"}'"
]
}
相关技能
supabase-audit-auth-config— 完整的身份验证配置supabase-audit-auth-signup— 注册流程测试supabase-report— 纳入最终报告