Confirmableのメール変更フローにおける競合状態の脆弱性を修正

heartcombo/devise

Deviseの Confirmable モジュールにおける「メールアドレス変更」フローに、攻撃者が自身のアカウントで他者のメールアドレスを確認できてしまう競合状態の脆弱性が発見されました。本PRは devise_unconfirmed_email_will_change! を導入し、unconfirmed_email が必ずSQLの UPDATE 文に含まれるよう強制することで、この脆弱性を修正します。

背景

Confirmable モジュールのメール変更フローにおいて、unconfirmed_email が「変更なし」とみなされた場合にSQLの UPDATE 文から除外されることを悪用した競合状態の脆弱性が存在していました。Issue #5783 で報告されたこの脆弱性は、以下の手順で悪用可能です:

  1. 攻撃者が attacker1@email.com でアカウントを登録する
  2. 攻撃者が attacker2@email.com へのメール変更を行うが、確認はまだしない
  3. 攻撃者が2つの並行した「メール変更」リクエストを送信する
    • リクエストA: attacker2@email.com への変更
    • リクエストB: victim@example.com への変更

リクエストBが先に完了すると、unconfirmed_email が被害者のアドレスに、confirmation_token が新しいトークンに更新されます。その後にリクエストAが処理されると、unconfirmed_email の値がメモリ上のモデルと同じ attacker2@email.com のため「変更なし」とみなされ、SQLは confirmation_token のみを更新します。結果として、データベースには「被害者のメールアドレス+攻撃者に送られたトークン」という状態が生まれ、攻撃者が確認リンクを踏むことで被害者のメールアドレスを自アカウントに確認済みとして設定できてしまいます。

この脆弱性はActive RecordのDirty Trackingの挙動に起因します。postpone_email_change_until_confirmation_and_regenerate_confirmation_token メソッドが self.unconfirmed_email = self.email をセットする際、ORMはメモリ上の現在値と比較して「変更なし」と判断すると、その属性をSQLの UPDATE から除外します。

技術的な変更

修正の核心は postpone_email_change_until_confirmation_and_regenerate_confirmation_token メソッドにおいて、unconfirmed_email を設定する直前に devise_unconfirmed_email_will_change! を呼び出し、ORMに「この属性は必ず更新する」と明示することです。

変更前:

def postpone_email_change_until_confirmation_and_regenerate_confirmation_token
  @reconfirmation_required = true
  self.unconfirmed_email = self.email
  self.email = self.devise_email_in_database
  self.confirmation_token = nil

変更後:

def postpone_email_change_until_confirmation_and_regenerate_confirmation_token
  @reconfirmation_required = true
  # Force unconfirmed_email to be updated, even if the value hasn't changed, to prevent a
  # race condition which could allow an attacker to confirm an email they don't own. See #5783.
  devise_unconfirmed_email_will_change!
  self.unconfirmed_email = self.email
  self.email = self.devise_email_in_database
  self.confirmation_token = nil

devise_unconfirmed_email_will_change! はActive RecordとMongoidのそれぞれに対応した実装が lib/devise/orm.rb に追加されました。Active Recordの実装は既存の will_change! パターンに沿ったシンプルな委譲です。

# Active Record
def devise_unconfirmed_email_will_change!
  unconfirmed_email_will_change!
end

Mongoidの実装では、will_change! だけでは未変更属性のSQL UPDATE への強制が不十分なため、changed_attributesnil を直接書き込むことで差分を強制的に発生させています。

# Mongoid
def devise_unconfirmed_email_will_change!
  # Mongoid's will_change! doesn't force unchanged attributes into updates,
  # so we override changed_attributes to make it see a difference.
  unconfirmed_email_will_change!
  changed_attributes["unconfirmed_email"] = nil
end

テストには競合状態を再現するケースが追加されています。Admin.where(id: attacker.id).update_all(...) でデータベースを直接更新してモデルのメモリ上の状態とDBの状態を乖離させた後、同じ値でのメール変更を実行し、最終的に被害者のメールアドレスが確認されないことを検証します。

設計判断

devise_unconfirmed_email_will_change! というラッパーメソッドを新設する方式 が採用されました。confirmable.rb に直接 unconfirmed_email_will_change! を呼び出すのではなく、ORM依存の処理を抽象化するレイヤーを挟むことで、Active RecordとMongoidの挙動の差異を吸収しています。

Mongoidについては、will_change! の呼び出しだけでは不十分であるというコメントが付いており、changed_attributes["unconfirmed_email"] = nil という追加の操作が必要になっています。既存の devise_will_save_change_to_email? などのラッパーメソッドと同じ命名規則に従うことで、ORM抽象化レイヤーの設計一貫性を維持しています。

修正箇所は1行の追加という最小限の変更でありながら、問題の根本であるDirty Trackingの挙動に直接対処しています。

まとめ

この修正は、ORMのDirty Tracking機構が「最適化」として行うSQL省略が、セキュリティ上の競合状態を生み出すという微妙な脆弱性への対処です。unconfirmed_email の更新を常に強制することで、並行リクエストによるトークンと未確認メールアドレスの不一致状態が発生しなくなり、Confirmableのメール変更フローの整合性が保証されます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
efe164c2

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)の3部構成が明確に適用されており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのIssue/PRリンク記法がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Deviseの内部実装、ORMのDirty Tracking、競合状態など、専門知識を持つエンジニアを対象とした適切な技術レベルと表現です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論→結論で構成され、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に可読性が高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiffの内容と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「Dirty Tracking」「競合状態」「will_change!」など、関連する技術用語が文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

競合状態の発生メカニズムから、ORMの挙動を利用した修正方法まで、技術的な説明は論理的で正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(脆弱性の内容、修正方法、設計判断)は、PRのDescriptionやDiff内のコード・コメントで裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#5784)とIssue番号(#5783)が正確に記載・リンクされています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトル「Confirmableのメール変更フローにおける競合状態の脆弱性を修正」は、PRの主題を正確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に含まれないバージョン情報やリリース予定などの外部知識は含まれておらず、信頼性が高いです。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

記事内の時間表現はPRの事実関係と一致しており、歪曲や誤解を招く表現はありません。