Skip to content

fix(backend): アカウント登録のemail enumerationを防ぐ#281

Open
yupix wants to merge 2 commits into
mainfrom
fix/26-email-enumeration
Open

fix(backend): アカウント登録のemail enumerationを防ぐ#281
yupix wants to merge 2 commits into
mainfrom
fix/26-email-enumeration

Conversation

@yupix

@yupix yupix commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

概要

新規登録 API (POST /v1/auth/register) で、メールアドレスが既に使用されていても未使用時と同一の 200 系レスポンスを返すようにしました。これまでは既存メールアドレスで登録すると 409 Conflict が返っており、レスポンスの違いからメールアドレスの登録有無が判別できてしまう問題(email enumeration)がありました。

Closes #26

変更内容

  • register ハンドラー(apps/backend/crates/handler/src/handlers/auth.rs): ユーザー挿入時に一意制約違反(AuthError::DuplicateEmail)を検知しても、これまでのように 409 を返さず、新規登録成功時と同一の 201 Created + "Register successful" を返すよう分岐を変更
  • 既存メールアドレス宛には確認メールの代わりに「既にアカウントがあります」通知メールを送信する新しい Apalis ジョブ already_registered_email を追加(apps/backend/crates/job/src/already_registered_email.rs
    • ペイロードは email のみ(トークン等の機微情報は含めない)。password_reset_email / verification_email の既存ジョブ実装パターンに従い、専用の Storage・ワーカー・JobState 経由の process を用意
    • メール本文の組み立ては service::already_registered_email_deliveryapps/backend/crates/service/src/already_registered_email_delivery.rs)に分離(password_reset_email_delivery 等と同様の構成)
    • ジョブペイロードに機微情報が混入しないことを確認する回帰テスト(キー集合の固定)を追加
  • ジョブ投入失敗時のレスポンス(503)は、新規登録・既存メールいずれの分岐でも同じ AuthError::VerificationEmailEnqueueFailed を使うことで、エラー発生時にも列挙対策が崩れないようにした
  • AppStatealready_registered_email_storage を追加し、main.rs / server.rs(ワーカー起動・グレースフルシャットダウン)/ 統合テスト共通ヘルパー (tests/common/mod.rs) に配線
  • Settingsalready_registered_email_worker_concurrency(デフォルト 1)を追加
  • OpenAPI ドキュメント(RegisterErrors)から 409 Conflict の定義を削除し、openapi.json を再生成(pnpm openapi:exportvp fmt
  • 統合テストハーネス(tests/common/mod.rsensure_schema)に system_settings のカラムデフォルトを追加。SeaORM の sync() は entity 定義からカラムデフォルトを生成しないため、register が呼ぶ ensure_system_settings_rowINSERT INTO system_settings (singleton)user_registration_enabled の NOT NULL 制約違反 (23502) → 500 になっていた(/register を叩く統合テストは本 PR が初のため、これまで顕在化していなかった)

挙動の変化

  • Before: 既存メールアドレスで POST /register すると 409 { "message": "email-already-exists" }
  • After: 既存メールアドレスでも新規登録時と同一の 201 "Register successful" を返す。既存アカウント宛には「登録済みです」通知メールが非同期で送信される(トークンなし、ログイン画面への案内のみ)
  • 新規メールアドレスでの登録フロー(確認メール送信等)は変更なし

テスト

  • cargo fmt --check / cargo check --workspace --all-targets / cargo clippy --workspace --all-targets — いずれも成功
  • cargo test --workspace --lib — 60件成功(新規ジョブのペイロード回帰テスト含む)
  • 統合テストをローカルで全件実行(CI と同じ postgres:17 / valkey 8.1 コンテナ + --test-threads=1): 37 本すべて PASS(新規追加の register_integration 含む)
  • apps/backend/tests/register_integration.rs を新規追加(新規メール登録時の 201、既存メール登録時も同一の 201・重複ユーザー行が作られないことを検証)
  • openapi.jsonpnpm openapi:export + vp fmt で再生成し、/register409 エントリ削除など意図した差分のみであることを確認済み

新規登録APIで、メールアドレスが既に使用されている場合も未使用時と
同一の201レスポンスを返すようにした。既存メール宛には確認メールの
代わりに「登録済みです」通知メールを送る(password_reset_email等の
既存ジョブ実装パターンに従い、専用の Apalis ジョブ/ワーカーを追加)。

Closes #26
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying koyori with  Cloudflare Pages  Cloudflare Pages

Latest commit: 27e2301
Status: ✅  Deploy successful!
Preview URL: https://d9f15e44.koyori.pages.dev
Branch Preview URL: https://fix-26-email-enumeration.koyori.pages.dev

View logs

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Coverage Report for apps/frontend

Status Category Percentage Covered / Total
🟢 Lines 97.34% 110 / 113
🟢 Statements 95.96% 119 / 124
🟢 Functions 95.55% 43 / 45
🟢 Branches 88.88% 48 / 54
File CoverageNo changed files found.
Generated in workflow #730 for commit 27e2301 by the Vitest Coverage Report Action

@chromatic-com

chromatic-com Bot commented Jul 10, 2026

Copy link
Copy Markdown

Tip

All tests passed and all changes approved!

🟢 UI Tests: 21 tests unchanged
🟢 UI Review: 21 stories published -- no changes
Storybook icon Storybook Publish: 21 stories published

@github-actions

Copy link
Copy Markdown

コード全体を確認しました。

レビュー結果

Medium

1. register エンドポイントにレート制限がなく、メールフラッディング攻撃の可能性

auth.rs:165-171 — 既存メールアドレスで POST /register を繰り返すと、「登録済み通知メール」がジョブキューに溜まり続け、最終的にすべて送信されます。resend_verification_emailauth.rs:242)は try_acquire_resend_slot で秒間送信を制限していますが、register には同等の仕組みがありません。

bcrypt の計算コスト(100〜500ms/req)で自然にスロットリングされますが、並列10接続でも1時間で数万通の送信要求を投入できてしまいます。攻撃者は既知のメールアドレスを指定するだけでよく、本PR以前(409で即終了・メール未送信)には存在しなかった攻撃面です。

try_acquire_resend_slot と同等のレート制限を register の duplicate パスに適用するか、少なくとも「同一メールアドレスへの連続送信を制限する」仕組みを検討してください。

Low

  • apps/frontend/stories/pages/SignUp.stories.ts:100-130RegisterError ストーリーが 409 をモックしていますが、本PRによりバックエンドは /register で 409 を返さなくなりました。テスト名(「登録エラー(409)」)とモックのステータスコードを更新してください。
  • auth.rs:119let user_id = Uuid::new_v4(); で生成した UUID が duplicate パスでは未使用になります。害はありませんが、insert_resultOk の場合にのみ使う変数としてスコープを狭めると意図が明確になります。

New%20session%20-%202026-07-10T01%3A10%3A08.718Z
opencode session  |  github run

register_integration が 500 で落ちていた原因の修正。テストハーネスの
ensure_schema は SeaORM の sync() でスキーマを作るが、sync() は entity
定義からカラムデフォルトを生成しないため、register ハンドラーの
ensure_system_settings_row が実行する
INSERT INTO system_settings (singleton) が user_registration_enabled の
NOT NULL 制約違反 (23502) になっていた。マイグレーション
(m20260520000000_initial_schema.rs)と同じカラムデフォルトを sync() 後に
付与して解消する。/register を叩く統合テストは今回が初のため、これまで
顕在化していなかった。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

アカウント新規登録の際、メアドが既に使用されていても200を返すようにする

1 participant