supabase-audit-auth-users
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUser Enumeration Audit
用户枚举漏洞审计
🔴 CRITICAL: PROGRESSIVE FILE UPDATES REQUIREDYou MUST write to context files AS YOU GO, not just at the end.
- Write to
IMMEDIATELY after each endpoint tested.sb-pentest-context.json- Log to
BEFORE and AFTER each test.sb-pentest-audit.log- DO NOT wait until the skill completes to update files
- If the skill crashes or is interrupted, all prior findings must already be saved
This is not optional. Failure to write progressively is a critical error.
This skill tests for user enumeration vulnerabilities in authentication flows.
🔴 CRITICAL: 需逐步更新文件你必须随时写入上下文文件,而不是只在最后写入。
- 测试每个端点后立即写入
.sb-pentest-context.json- 每次测试前后都要记录到
.sb-pentest-audit.log- 禁止等到技能完成后再更新文件
- 如果技能崩溃或被中断,所有已有的发现必须已保存
这不是可选要求。不逐步写入属于严重错误。
本技能用于测试认证流程中的用户枚举漏洞。
When to Use This Skill
何时使用本技能
- To check if user existence can be detected
- To test login, signup, and recovery flows for information leakage
- As part of authentication security audit
- Before production deployment
- 检查是否可以检测到用户账号是否存在
- 测试登录、注册和找回密码流程中的信息泄露问题
- 作为认证安全审计的一部分
- 生产部署前的检测
Prerequisites
前提条件
- Supabase URL and anon key available
- Auth endpoints accessible
- 已获取Supabase URL和匿名密钥(anon key)
- 认证端点可访问
What is User Enumeration?
什么是用户枚举?
User enumeration occurs when an application reveals whether a user account exists through:
| Vector | Indicator |
|---|---|
| Different error messages | "User not found" vs "Wrong password" |
| Response timing | Fast for non-existent, slow for existing |
| Response codes | 404 vs 401 |
| Signup response | "Email already registered" |
用户枚举指应用通过以下方式泄露用户账号是否存在的信息:
| 攻击向量 | 识别指标 |
|---|---|
| 不同的错误提示 | "用户不存在" vs "密码错误" |
| 响应时间 | 不存在的用户响应快,存在的用户响应慢 |
| 响应状态码 | 404 vs 401 |
| 注册响应 | "该邮箱已注册" |
Why It Matters
为什么这很重要
| Risk | Impact |
|---|---|
| Targeted attacks | Attackers know valid accounts |
| Phishing | Confirm targets have accounts |
| Credential stuffing | Reduce attack scope |
| Privacy | Reveal user presence |
| 风险 | 影响 |
|---|---|
| 定向攻击 | 攻击者知晓有效账号 |
| 钓鱼攻击 | 确认目标拥有账号 |
| 凭证填充攻击 | 缩小攻击范围 |
| 隐私问题 | 泄露用户是否使用该服务 |
Tests Performed
执行的测试
| Endpoint | Test Method |
|---|---|
| Try registering existing email |
| Try login with various emails |
| Try password reset |
| Try OTP for various emails |
| 端点 | 测试方法 |
|---|---|
| 尝试注册已存在的邮箱 |
| 使用不同邮箱尝试登录 |
| 尝试重置密码 |
| 使用不同邮箱请求OTP |
Usage
使用方法
Basic Enumeration Test
基础枚举测试
Test for user enumeration vulnerabilities测试用户枚举漏洞Test Specific Endpoint
测试特定端点
Test login endpoint for user enumeration测试登录端点的用户枚举漏洞Output Format
输出格式
═══════════════════════════════════════════════════════════
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": "target@example.com", "password": "test123"}'
# If user exists: {"msg": "User already registered"}
# If new user: User created or confirmation needed─────────────────────────────────────────────────────────
Login Endpoint (/auth/v1/token)
─────────────────────────────────────────────────────────
Test: POST with different email scenarios
Existing email, wrong password:
├── Response: {"error": "Invalid login credentials"}
├── Time: 245ms
└── Code: 400
Non-existing email:
├── Response: {"error": "Invalid login credentials"}
├── Time: 52ms ← Significantly faster!
└── Code: 400
Status: 🟠 P2 - ENUMERABLE VIA TIMING
Although the error message is the same, the response
time is noticeably different:
├── Existing user: ~200-300ms (password hashing)
└── Non-existing: ~50-100ms (no hash check)
Timing Attack PoC:
python
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('target@example.com')─────────────────────────────────────────────────────────
Password Recovery (/auth/v1/recover)
─────────────────────────────────────────────────────────
Test: POST recovery request for different emails
Existing email:
├── Response: {"message": "Password recovery email sent"}
├── Time: 1250ms (email actually sent)
└── Code: 200
Non-existing email:
├── Response: {"message": "Password recovery email sent"}
├── Time: 85ms ← Much faster (no email sent)
└── Code: 200
Status: 🟠 P2 - ENUMERABLE VIA TIMING
Same message, but timing reveals existence.
Existing users trigger actual email sending (~1s+).
─────────────────────────────────────────────────────────
Magic Link / OTP (/auth/v1/otp)
─────────────────────────────────────────────────────────
Test: Request OTP for different emails
Existing email:
├── Response: {"message": "OTP sent"}
├── Time: 1180ms
└── Code: 200
Non-existing email:
├── Response: {"error": "User not found"}
├── Time: 95ms
└── Code: 400
Status: 🔴 P1 - DIRECTLY ENUMERABLE
The error message explicitly states user doesn't exist.
─────────────────────────────────────────────────────────
Summary
─────────────────────────────────────────────────────────
Endpoints Tested: 4
Enumerable: 4 (100%)
Vulnerability Severity:
├── 🔴 P1: OTP endpoint (explicit message)
├── 🟠 P2: Signup endpoint (explicit message)
├── 🟠 P2: Login endpoint (timing attack)
└── 🟠 P2: Recovery endpoint (timing attack)
Overall User Enumeration Risk: HIGH
An attacker can determine if any email address
has an account in your application.
─────────────────────────────────────────────────────────
Mitigation Recommendations
─────────────────────────────────────────────────────────
-
CONSISTENT RESPONSES Return identical messages for all scenarios: "If an account exists, you will receive an email"
-
CONSISTENT TIMING Add artificial delay to normalize response times:typescript
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; -
RATE LIMITING Already enabled: 3/hour per IP Consider per-email rate limiting too.
-
CAPTCHA Add CAPTCHA for repeated attempts:
- After 3 failed logins
- For password recovery
- For signup
-
MONITORING Alert on enumeration patterns:
- Many requests with different emails
- Sequential email patterns (user1@, user2@, ...)
═══════════════════════════════════════════════════════════
undefined═══════════════════════════════════════════════════════════
用户枚举漏洞审计
═══════════════════════════════════════════════════════════
项目: abc123def.supabase.co
─────────────────────────────────────────────────────────
注册端点 (/auth/v1/signup)
─────────────────────────────────────────────────────────
测试: 使用已知已存在的邮箱发起POST请求
已存在用户的响应: "用户已注册"
新邮箱的响应: 返回用户对象
状态: 🟠 P2 - 可枚举
响应明确指示该邮箱是否已注册。
利用示例:
```bash
curl -X POST https://abc123def.supabase.co/auth/v1/signup \
-H "apikey: [anon-key]" \
-H "Content-Type: application/json" \
-d '{"email": "target@example.com", "password": "test123"}'
# 若用户存在: {"msg": "User already registered"}
# 若为新用户: 用户创建成功或需要确认─────────────────────────────────────────────────────────
登录端点 (/auth/v1/token)
─────────────────────────────────────────────────────────
测试: 针对不同邮箱场景发起POST请求
已存在邮箱,密码错误:
├── 响应: {"error": "Invalid login credentials"}
├── 响应时间: 245ms
└── 状态码: 400
不存在的邮箱:
├── 响应: {"error": "Invalid login credentials"}
├── 响应时间: 52ms ← 明显更快!
└── 状态码: 400
状态: 🟠 P2 - 可通过时序攻击枚举
虽然错误提示相同,但响应时间存在明显差异:
├── 已存在用户: ~200-300ms(密码哈希验证)
└── 不存在用户: ~50-100ms(无哈希验证)
时序攻击PoC:
python
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 # 阈值
exists = check_user('target@example.com')─────────────────────────────────────────────────────────
密码找回 (/auth/v1/recover)
─────────────────────────────────────────────────────────
测试: 针对不同邮箱发起找回请求
已存在邮箱:
├── 响应: {"message": "Password recovery email sent"}
├── 响应时间: 1250ms(实际发送了邮件)
└── 状态码: 200
不存在的邮箱:
├── 响应: {"message": "Password recovery email sent"}
├── 响应时间: 85ms ← 快得多(未发送邮件)
└── 状态码: 200
状态: 🟠 P2 - 可通过时序攻击枚举
提示信息相同,但响应时间泄露了用户是否存在。
已存在用户会触发实际邮件发送(~1秒以上)。
─────────────────────────────────────────────────────────
魔术链接/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: 找回密码端点(时序攻击)
整体用户枚举风险: 高
攻击者可以确定任意邮箱是否在你的应用中拥有账号。
─────────────────────────────────────────────────────────
修复建议
─────────────────────────────────────────────────────────
-
统一响应 所有场景返回相同的提示信息: "如果账号存在,你将收到一封邮件"
-
统一响应时间 添加人工延迟以标准化响应时间:typescript
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@, ...)
═══════════════════════════════════════════════════════════
undefinedTiming Analysis
时序分析
The skill measures response times to detect timing-based enumeration:
Existing user:
├── Password hash verification: ~200-300ms
├── Email sending: ~1000-2000ms
└── Database lookup: ~5-20ms
Non-existing user:
├── No hash verification: 0ms
├── No email sending: 0ms
└── Database lookup: ~5-20ms (not found)Threshold detection:
- Difference > 100ms: Possible timing leak
- Difference > 500ms: Definite timing leak
本技能通过测量响应时间来检测基于时序的枚举漏洞:
已存在用户:
├── 密码哈希验证: ~200-300ms
├── 邮件发送: ~1000-2000ms
└── 数据库查询: ~5-20ms
不存在用户:
├── 无哈希验证: 0ms
├── 无邮件发送: 0ms
└── 数据库查询: ~5-20ms(未找到)阈值检测:
- 差异>100ms: 可能存在时序泄露
- 差异>500ms: 确定存在时序泄露
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
}
]
}
}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
}
]
}
}Mitigation Code Examples
修复代码示例
Consistent Response Time
统一响应时间
typescript
// 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"
}));
}
});typescript
// 标准化响应时间的边缘函数
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: "如果你有账号,请检查你的邮箱"
}));
}
});Generic Error Messages
通用错误提示
typescript
// 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;
}typescript
// 不泄露用户是否存在
async function requestPasswordReset(email: string) {
// 始终返回成功提示
const response = {
message: "如果该邮箱对应的账号存在," +
"你将收到一封密码重置链接。"
};
// 在后台执行实际重置操作(不等待)
supabase.auth.resetPasswordForEmail(email).catch(() => {});
return response;
}MANDATORY: Progressive Context File Updates
必须:逐步更新上下文文件
⚠️ This skill MUST update tracking files PROGRESSIVELY during execution, NOT just at the end.
⚠️ 本技能必须在执行过程中逐步更新跟踪文件,而不是只在最后更新。
Critical Rule: Write As You Go
关键规则:随时写入
DO NOT batch all writes at the end. Instead:
- Before testing each endpoint → Log the action to
.sb-pentest-audit.log - After each timing measurement → Immediately update
.sb-pentest-context.json - After each enumeration vector found → Log the finding immediately
This ensures that if the skill is interrupted, crashes, or times out, all findings up to that point are preserved.
禁止批量在最后写入。请按以下方式操作:
- 测试每个端点前 → 将操作记录到
.sb-pentest-audit.log - 每次时序测量后 → 立即更新
.sb-pentest-context.json - 每次发现枚举向量后 → 立即记录该发现
这样可以确保如果技能被中断、崩溃或超时,截至该点的所有发现都已被保存。
Required Actions (Progressive)
必须执行的操作(逐步)
-
Updatewith results:
.sb-pentest-context.jsonjson{ "user_enumeration": { "timestamp": "...", "endpoints_tested": 4, "vulnerabilities": [ ... ] } } -
Log to:
.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 -
If files don't exist, create them before writing.
FAILURE TO UPDATE CONTEXT FILES IS NOT ACCEPTABLE.
-
更新记录结果:
.sb-pentest-context.jsonjson{ "user_enumeration": { "timestamp": "...", "endpoints_tested": 4, "vulnerabilities": [ ... ] } } -
记录到:
.sb-pentest-audit.log[TIMESTAMP] [supabase-audit-auth-users] [START] 测试用户枚举漏洞 [TIMESTAMP] [supabase-audit-auth-users] [FINDING] P1: OTP端点可枚举 [TIMESTAMP] [supabase-audit-auth-users] [CONTEXT_UPDATED] .sb-pentest-context.json已更新 -
如果文件不存在,在写入前创建它们。
不更新上下文文件是不被接受的。
MANDATORY: Evidence Collection
必须:收集证据
📁 Evidence Directory:
.sb-pentest-evidence/05-auth-audit/enumeration-tests/📁 证据目录:
.sb-pentest-evidence/05-auth-audit/enumeration-tests/Evidence Files to Create
需要创建的证据文件
| File | Content |
|---|---|
| Login endpoint timing analysis |
| Recovery endpoint timing |
| OTP endpoint message analysis |
| 文件 | 内容 |
|---|---|
| 登录端点时序分析 |
| 找回密码端点时序分析 |
| OTP端点提示信息分析 |
Evidence Format
证据格式
json
{
"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": "definitely-not-exists@example.com",
"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\": \"existing@example.com\", \"password\": \"wrong\"}'",
"# Timing test - non-existing user\ntime curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"nonexistent@example.com\", \"password\": \"wrong\"}'"
]
}json
{
"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": "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": [
"# 时序测试 - 已存在用户\ntime curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"existing@example.com\", \"password\": \"wrong\"}'",
"# 时序测试 - 不存在用户\ntime curl -X POST '$URL/auth/v1/token?grant_type=password' -H 'apikey: $ANON_KEY' -d '{\"email\": \"nonexistent@example.com\", \"password\": \"wrong\"}'"
]
}Related Skills
相关技能
- — Full auth configuration
supabase-audit-auth-config - — Signup flow testing
supabase-audit-auth-signup - — Include in final report
supabase-report
- — 完整认证配置审计
supabase-audit-auth-config - — 注册流程测试
supabase-audit-auth-signup - — 包含在最终报告中
supabase-report