名称: supabase-audit-auth-users 描述: 通过多种身份认证端点测试用户枚举漏洞。
用户枚举审计
🔴 关键: 需要渐进式文件更新
您必须在执行过程中写入上下文文件,而不是仅在结束时。
- 测试每个端点后立即写入
.sb-pentest-context.json- 每次测试前后记录到
.sb-pentest-audit.log- 不要等待技能完成再更新文件
- 如果技能崩溃或中断,所有先前发现必须已保存
这是强制性的。渐进式更新失败是关键错误。
此技能测试身份认证流程中的用户枚举漏洞。
何时使用此技能
- 检查是否可以检测用户存在
- 测试登录、注册和恢复流程的信息泄露
- 作为身份认证安全审计的一部分
- 生产部署前
先决条件
- Supabase URL和匿名密钥可用
- 身份认证端点可访问
什么是用户枚举?
用户枚举指应用程序通过以下方式揭示用户账户是否存在:
| 向量 | 指示器 |
|---|---|
| 不同的错误消息 | “用户未找到” vs “密码错误” |
| 响应时间 | 非存在用户快,存在用户慢 |
| 响应代码 | 404 vs 401 |
| 注册响应 | “电子邮件已注册” |
为何重要
| 风险 | 影响 |
|---|---|
| 定向攻击 | 攻击者知道有效账户 |
| 钓鱼 | 确认目标有账户 |
| 凭据填充 | 减少攻击范围 |
| 隐私 | 揭示用户存在 |
执行的测试
| 端点 | 测试方法 |
|---|---|
/auth/v1/signup |
尝试注册现有电子邮件 |
/auth/v1/token |
尝试使用各种电子邮件登录 |
/auth/v1/recover |
尝试密码重置 |
/auth/v1/otp |
尝试各种电子邮件的OTP |
用法
基本枚举测试
测试用户枚举漏洞
测试特定端点
测试登录端点的用户枚举
输出格式
═══════════════════════════════════════════════════════════
用户枚举审计
═══════════════════════════════════════════════════════════
项目: abc123def.supabase.co
─────────────────────────────────────────────────────────
注册端点 (/auth/v1/signup)
─────────────────────────────────────────────────────────
测试: POST与已知现有电子邮件
现有响应: "用户已注册"
新电子邮件响应: 返回用户对象
状态: 🟠 P2 - 可枚举
响应清晰指示电子邮件是否注册。
利用:
```bash
curl -X POST https://abc123def.supabase.co/auth/v1/signup \
-H "apikey: [匿名密钥]" \
-H "Content-Type: application/json" \
-d '{"email": "target@example.com", "password": "test123"}'
# 如果用户存在: {"msg": "用户已注册"}
# 如果新用户: 用户创建或需要确认
───────────────────────────────────────────────────────── 登录端点 (/auth/v1/token) ─────────────────────────────────────────────────────────
测试: POST不同电子邮件场景
现有电子邮件,错误密码: ├── 响应: {“error”: “无效登录凭据”} ├── 时间: 245ms └── 代码: 400
非存在电子邮件: ├── 响应: {“error”: “无效登录凭据”} ├── 时间: 52ms ← 显著更快! └── 代码: 400
状态: 🟠 P2 - 通过时间可枚举
尽管错误消息相同,但响应时间明显不同: ├── 现有用户: ~200-300ms (密码哈希) └── 非存在用户: ~50-100ms (无哈希检查)
时间攻击PoC:
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': '[匿名密钥]'}
)
elapsed = time.time() - start
return elapsed > 0.15 # 阈值
exists = check_user('target@example.com')
───────────────────────────────────────────────────────── 密码恢复 (/auth/v1/recover) ─────────────────────────────────────────────────────────
测试: POST恢复请求不同电子邮件
现有电子邮件: ├── 响应: {“message”: “密码恢复邮件已发送”} ├── 时间: 1250ms (实际发送邮件) └── 代码: 200
非存在电子邮件: ├── 响应: {“message”: “密码恢复邮件已发送”} ├── 时间: 85ms ← 更快 (未发送邮件) └── 代码: 200
状态: 🟠 P2 - 通过时间可枚举
相同消息,但时间揭示存在。 现有用户触发实际邮件发送 (~1s+)。
───────────────────────────────────────────────────────── 魔法链接 / OTP (/auth/v1/otp) ─────────────────────────────────────────────────────────
测试: 请求OTP不同电子邮件
现有电子邮件: ├── 响应: {“message”: “OTP已发送”} ├── 时间: 1180ms └── 代码: 200
非存在电子邮件: ├── 响应: {“error”: “用户未找到”} ├── 时间: 95ms └── 代码: 400
状态: 🔴 P1 - 直接可枚举
错误消息明确说明用户不存在。
───────────────────────────────────────────────────────── 总结 ─────────────────────────────────────────────────────────
测试端点: 4 可枚举: 4 (100%)
漏洞严重性: ├── 🔴 P1: OTP端点 (明确消息) ├── 🟠 P2: 注册端点 (明确消息) ├── 🟠 P2: 登录端点 (时间攻击) └── 🟠 P2: 恢复端点 (时间攻击)
整体用户枚举风险: 高
攻击者可以确定任何电子邮件地址在您应用中是否有账户。
───────────────────────────────────────────────────────── 缓解建议 ─────────────────────────────────────────────────────────
-
一致响应 返回所有场景相同消息: “如果账户存在,您将收到电子邮件”
-
一致时间 添加人工延迟以标准化响应时间:
const MIN_RESPONSE_TIME = 1000; // 1秒 const start = Date.now(); // ... 执行身份认证操作 ... 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@, …)
═══════════════════════════════════════════════════════════
## 时间分析
此技能测量响应时间以检测基于时间的枚举:
现有用户: ├── 密码哈希验证: ~200-300ms ├── 邮件发送: ~1000-2000ms └── 数据库查找: ~5-20ms
非存在用户: ├── 无哈希验证: 0ms ├── 无邮件发送: 0ms └── 数据库查找: ~5-20ms (未找到)
阈值检测:
- 差异 > 100ms: 可能时间泄露
- 差异 > 500ms: 确定时间泄露
## 上下文输出
```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
}
]
}
}
缓解代码示例
一致响应时间
// 边缘函数与标准化时间
const MIN_RESPONSE_TIME = 1500; // 1.5秒
Deno.serve(async (req) => {
const start = Date.now();
try {
// 执行实际身份认证操作
const result = await handleAuth(req);
// 标准化响应时间
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) {
// 错误的相同时间
const elapsed = Date.now() - start;
await new Promise(r => setTimeout(r,
Math.max(0, MIN_RESPONSE_TIME - elapsed)
));
// 通用错误消息
return new Response(JSON.stringify({
message: "如果有账户,请检查您的电子邮件"
}));
}
});
通用错误消息
// 不要揭示用户存在
async function requestPasswordReset(email: string) {
// 始终返回成功消息
const response = {
message: "如果该电子邮件有账户,您将收到密码重置链接。"
};
// 后台执行实际重置 (不等待)
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:[时间戳] [supabase-audit-auth-users] [开始] 测试用户枚举 [时间戳] [supabase-audit-auth-users] [发现] P1: OTP端点可枚举 [时间戳] [supabase-audit-auth-users] [上下文更新] .sb-pentest-context.json更新 -
如果文件不存在, 写入前创建它们。
更新上下文文件失败不可接受。
强制: 证据收集
📁 证据目录: .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": "[已知存在]@example.com",
"response_time_ms": 245,
"response": {"error": "Invalid login credentials"}
},
"nonexisting_user_test": {
"email": "definitely-not-exists@example.com",
"response_time_ms": 52,
"response": {"error": "Invalid login credentials"}
},
"timing_difference_ms": 193,
"result": "ENUMERABLE",
"impact": "可以通过时间确定电子邮件是否有账户"
},
{
"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": "错误消息明确揭示用户存在"
}
],
"curl_commands": [
"# 时间测试 - 现有用户
time curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"existing@example.com\", \"password\": \"wrong\"}'",
"# 时间测试 - 非存在用户
time curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"nonexistent@example.com\", \"password\": \"wrong\"}'"
]
}
相关技能
supabase-audit-auth-config— 完整身份认证配置supabase-audit-auth-signup— 注册流程测试supabase-report— 包含在最终报告中