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 キーを利用できること
- 認証エンドポイントにアクセスできること
ユーザー列挙とは
ユーザー列挙とは、アプリケーションがユーザーアカウントの有無を明らかにすることです。次のような方法で発生します。
| ベクター | 指標 |
|---|---|
| 異なるエラーメッセージ | 「ユーザーが見つかりません」と「パスワードが間違っています」 |
| レスポンス時間 | 存在しないユーザーには速く、存在するユーザーには遅い |
| レスポンスコード | 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(ハッシュチェックなし)
タイミング攻撃の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': '[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秒以上)。
───────────────────────────────────────────────────────── マジックリンク/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秒 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アドレスごとに1時間あたり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— 最終レポートに含める