CyberFix Note
セキュアコーディング

パスワード再設定フローの安全な設計。トークンと応答と復旧経路の要件を一次情報で整理

対象の目安: 認証機能を実装するWebアプリ開発者 / 実務

ソウ攻撃・脆弱性リサーチ担当
・ 約36分で読めます
パスワード再設定フローの安全な設計。トークンと応答と復旧経路の要件を一次情報で整理

ログイン画面は堅くできます。パスワードを強い関数でハッシュ化し、総当たりに絞りをかけ、多要素認証を必須にすれば、正面から入るのは難しくなります。ところが同じ画面の下には「パスワードをお忘れの方」というリンクがあり、そこを押すとメールが1通届いて、リンクを開けば新しいパスワードを設定できてしまいます。メールだけで再設定できる実装では、この経路に多要素認証の要求も端末の確認も入っていません。正面から入るより手数が少ないぶん、こちらが狙われやすくなります。

OWASPのAPI Security Top 10は、この関係を短く言い切っています。認証のエンドポイントとフローは守るべき資産であり、「Forgot password / reset password」も認証機構と同じ扱いにしなければならない、というものです。再設定フローは補助機能ではなく、パスワードを知らなくても本人になれる二つ目の認証経路です。強度は弱いほうの経路に揃います。

この構図はSIMスワップやリアルタイムフィッシングと直結します。再設定コードの送り先が電話番号なら、番号を奪えば再設定できます。送り先がメールなら、メールアカウントが実質的に最上位の資格情報になります。この記事では、認証機能を実装する開発者に向けて、パスワード再設定とアカウント復旧のフローを、OWASPとNISTとMITREとIPAの一次情報にもとづいて設計要件から実装の細部まで整理します。

強い認証を迂回する二つ目の入口

認証の設計を「ログイン画面の強度」として考えると穴が残ります。アカウントへ到達できる経路は、通常のログイン、パスワード再設定、多要素認証の再登録、ヘルプデスクへの依頼、管理者による代理操作と複数あり、それぞれが独立した検証ロジックを持つためです。ASVS 5.0.0の6.3.4は、複数の認証経路を持つアプリケーションについて、文書化されていない経路が存在しないことと、セキュリティ制御と認証強度が一貫して適用されることを求めています。経路ごとに強度がばらつく実装が現実に多いためです。

MITREのCWE-640は、この弱点を「忘れたパスワードのための脆弱なパスワード復旧メカニズム」として定義しています。元のパスワードを知らなくても利用者がパスワードを復旧または変更できる仕組みを製品が持つが、その仕組みが弱い状態、という定義です。設計を評価する軸は「再設定が便利かどうか」ではなく、「再設定が通常の認証と同じかそれ以上の確からしさで本人を確認しているか」になります。

CWE-640は、脆弱なパスワード復旧の仕組みが強固なパスワード認証の仕組みを完全に無効化すると述べ、想定される弱点として、秘密の質問が推測しやすいこと、実装上の欠陥で新しいパスワードが本人以外のアドレスへ送られること、再設定の頻度に絞りがなくサービス妨害に使えること、新しい一時パスワードを生成せず元のパスワードを送ることを挙げています。

NIST SP 800-63B-4の区分も整理に役立ちます。同文書は「アカウント復旧」を、認証に必要な認証子の管理を失った状態からの回復と定義したうえで、他の認証子でまだ認証できる利用者が忘れたパスワードを置き換える行為は、復旧ではなく新しい認証子のバインドにあたる、と切り分けています。両者を同じ「パスワードをお忘れの方」に束ねると、要求すべき確認の強度が低いほうに引きずられます。

認証と認可の違いを含めた前提の整理は、こちらの記事にまとめています。

あわせて読みたい

認証と認可の違い。Authentication と Authorization を基礎から整理する

再設定フローに集まる攻撃の型

再設定フローへの攻撃は、実装のどの部分が原因で成立するかで分類できます。分類しておくと、対策が思いつきではなく設計要件として並びます。

攻撃成立させる実装起きること対策の方向
ユーザー列挙存在するアドレスと存在しないアドレスで文言やHTTPステータスや応答時間が異なる有効な利用者の一覧が作られる文言とステータスと処理時間を一致させる
トークン推測連番、時刻由来、短い値、弱い乱数によるトークン生成他人のトークンを当てて任意のアカウントを奪われる暗号論的に安全な乱数生成器で十分な長さを確保する
トークン漏えいURLからの外部参照、アクセスログ、メール転送設定、共有端末の履歴有効なトークンがそのまま再利用されるReferrer-Policy: no-referrerの設定、ログへの非記録、短い期限
再利用と競合使用済み判定と更新が分かれ、同じトークンで複数の要求が通る一度使ったリンクが二度目も通る使用済みの更新を原子的な条件付き更新にする
総当たりと大量送信要求と検証にレート制限がない短いコードを総当たりされる。受信箱が要求で埋まるアカウント単位のレート制限、CAPTCHA
秘密の質問知識ベース認証を単独の本人確認に使う公開情報から答えを得た第三者に通過される認証手段から外す
人手の復旧の悪用ヘルプデスクが電話の申告だけで再設定に応じる説得だけで本人確認が突破される確認手順を文書化し、断り方まで定める

OWASPのForgot Password Cheat Sheetは、再設定要求の段階でアカウント単位のレート制限やCAPTCHAといった過剰な自動送信への防御を実装することを求めています。理由として、そうしなければ攻撃者が特定のアカウントに1時間あたり数千回の再設定要求を送り、利用者の受信箱を無意味な要求で溢れさせられる、と説明しています。

OWASP API Security Top 10 2023のAPI2:2023 Broken Authenticationは、防止策として「資格情報の復旧および忘れたパスワードのエンドポイントは、総当たり、レート制限、ロックアウトの保護という点でログインのエンドポイントと同じように扱うべきである」と記述し、メールアドレスや二要素認証の電話番号の変更のような機微な操作では再認証を要求することを挙げています。

ロックアウトの扱いには注意が要ります。同シートは、忘れたパスワードへの攻撃に応じてアカウントをロックアウトすべきではない、と明記しています。利用者名が知られているアカウントに対して、攻撃者が再設定機能を叩き続けるだけで正当な利用者を締め出せてしまうためです。冒頭の指針でも、有効なトークンが提示されるまではアカウントに変更を加えないこと、その例としてロックアウトを挙げています。

トークンとコードの生成と保管

再設定用の値には、URLに載せるトークンと、SMSなどの別経路で送るPINやコードの2種類があります。OWASPのForgot Password Cheat Sheetは、どちらにも共通の要件を挙げています。暗号論的に安全な乱数生成器で生成すること、総当たりから守れる十分な長さであること、データベース上で個々の利用者と紐づいていること、使用後に無効化されること、パスワード保管と同じ考え方で安全に保管することです。PINについては、6桁から12桁の数字でSMSのような副経路で送るもの、と説明しています。

同シートはURLトークンについて、再設定URLの生成時にHostヘッダに依存せず、URLをハードコードするか信頼できるドメインの一覧と照合すること、URLがHTTPSであること、再設定ページにReferrer Policyを設定して参照元の漏えいを避けること(原文の表記はnoreferrer値)を挙げています。あわせて、完了後に利用者を自動的にログインさせないこと、再設定された旨のメールを送ること(パスワードを含めないこと)を求めています。

「安全に保管する」は、実装では平文で持たないという意味になります。データベースが読まれた時点で、保存されたトークンはそのまま乗っ取りに使えるからです。ハッシュ化して保管し、照合はハッシュ同士で行います。トークン自体が高いエントロピーを持つ乱数であれば、パスワードほど遅いハッシュ関数は要りません。一方、手入力する短いPINは総当たりの対象になるため、試行回数の制限で守ります。NIST SP 800-63B-4は、発行型と保存型のどちらの復旧コードも、検証をレート制限の要件の対象にすると定めています。

有効期限は「短くする」とだけ言われがちですが、NIST SP 800-63B-4は送付経路ごとに上限を数値で示しています。

復旧コードの送付先NIST SP 800-63B-4が定める有効期限の上限
SMSまたは音声通話10分
電子メール24時間
米国本土内の郵便住所21日
米国本土外の郵便住所30日

同文書は発行型の復旧コードに、承認された乱数生成器由来の6桁以上の10進数(または同等のもの)を求めています。登録時に利用者へ渡して手元で保管してもらう保存型の復旧コードには、より厳しい要件が置かれます。承認された乱数生成器から64ビット以上を含むこと、加入者アカウント内では承認された一方向関数でハッシュ化した形で保管すること、使用後はそのコードを無効化して新しい保存型復旧コードを発行することです。バックアップコードを実装するなら、使い切りのリストを配って終わりではなく、1枚使われたら補充するところまでが要件です。

初期パスワードの発行も同じ設計思想の対象です。IPAの「安全なウェブサイトの作り方」改訂第7版は、初期パスワードを暗号論的擬似乱数生成器で規則性をなくして発行することを挙げています。ASVS 5.0.0の6.4.1も、システムが生成する初期パスワードや有効化コードが、安全に乱数生成され、短期間で、あるいは最初の使用後に失効することを求め、これらが長期のパスワードになってはならないと明記しています。

パスワード本体の保管については、こちらで基礎から扱っています。

あわせて読みたい

パスワードはどう保存すべきか。ハッシュ化とソルトとストレッチングの基礎

存在するアカウントを言い当てさせない応答

再設定フォームは、メールアドレスを入れれば「そのアドレスが登録されているか」を教えてくれる装置になりがちです。OWASPのAuthentication Cheat Sheetは、パスワード復旧の応答について、誤った例として「We just sent you a password reset link.」と「This email address doesn't exist in our database.」を挙げ、正しい例として「If that email address is in our database, we will send you an email to reset your password.」を挙げています。IPAの「安全なウェブサイトの作り方」も、認証画面のエラーメッセージには「ユーザIDもしくはパスワードが違います」のような表現を勧めています。

文言を揃えても、差は3か所に残ります。1つ目はHTTPステータスコードです。同シートは、共通のエラーページを表示していても成功で200、失敗で403のようにステータスが異なると、アカウントの有効性が漏れると指摘しています。2つ目は応答時間で、実装によっては成功と失敗で処理時間が大きく異なり、時間ベースの攻撃が成立すると述べています。MITREはこの食い違いをCWE-204(Observable Response Discrepancy)として整理しています。3つ目はレスポンスの副次的な差で、リダイレクト先、Set-Cookieの有無、ボディの長さが該当します。

OWASP ASVS 5.0のV6 Authenticationにある6.3.8は、エラーメッセージ、HTTPレスポンスコード、応答時間の差などによって、失敗した認証チャレンジから有効な利用者を推測できないことを検証項目とし、登録機能と忘れたパスワードの機能もこの保護を備えなければならないと明記しています(レベル3)。

応答時間を揃える実装は、条件分岐で早期に抜けないところから始まります。OWASPのForgot Password Cheat Sheetは、早期リターンではなく同じロジックをたどるようにするか、非同期呼び出しにすることで応答時間の一定化を達成できると述べています。実務では、メール送信を非同期のジョブに逃がし、リクエストの処理はアカウントの有無にかかわらず同じ手順を通す形が扱いやすくなります。ダミーの計算で時間を合わせる方法もありますが、計測して差が消えたことを確かめなければ意味がありません。

注意

再設定要求のレスポンスを一致させても、リンクを開いた後の画面で「このアカウントは存在しません」と表示すれば同じ情報が漏れます。列挙対策は、要求、検証、完了のすべての画面と、そこから呼ばれるAPIの応答を通して確認します。

再設定が終わった瞬間に止めるもの

新しいパスワードを保存したら処理は終わり、とはなりません。攻撃者が先にセッションを奪っていた場合、パスワードを変えても古いセッションが生き残り、侵入は続きます。OWASPのForgot Password Cheat Sheetは、再設定の完了時に既存のセッションをすべて無効化するかどうかを利用者に尋ねるか、自動的に無効化することを挙げています。設定後は通常の仕組みでログインさせ、自動的にログイン状態にしないことも求めています。認証とセッション処理のコードが複雑になり、脆弱性を作り込む可能性が上がるためです。

OWASPのSession Management Cheat Sheetは、権限レベルが変わる場面でのセッションIDの再生成を必須とし、考慮すべき場面にパスワードの変更を含めています。無効化はサーバ側での実施が必須で、クライアント側でCookieを消すだけでは足りない点も明記されています。IPAの「安全なウェブサイトの作り方」も、ログインが成功した時点から新しいセッションを開始することを挙げています。

止めるべき対象はブラウザのセッションだけではありません。次の一覧を実装ごとに作っておくと漏れが減ります。

  1. 1

    サーバ側セッションの一括無効化

    対象利用者のセッションをすべて失効させます。セッションストアが利用者IDで引ける構造かを設計時に確認します。
  2. 2

    リフレッシュトークンの失効

    アクセストークンが短命でも、リフレッシュトークンが生きていれば新しいトークンが発行され続けます。利用者単位で失効できる管理を用意します。
  3. 3

    永続ログイン用トークンとAPIキーの整理

    長期Cookieや端末記憶のトークンはパスワードとは独立に生き残ります。APIキーを残す場合は、利用者に一覧を提示して確認を促します。
  4. 4

    未使用の再設定トークンの無効化

    同じ利用者に発行済みで未使用のトークンを失効させ、攻撃者が先に取得した古いリンクを塞ぎます。第三者が要求を繰り返して直前のリンクを無効化し続けられないよう、アカウント単位のレート制限と組み合わせます。
  5. 5

    通知の送信

    登録済みの通知先へ、再設定の事実と、心当たりがない場合の連絡先を記載した通知を送ります。

通知は形式的な作業に見えますが、規格上は必須の要素です。NIST SP 800-63B-4は、アカウント復旧はどの場合でも加入者またはその指定者への通知を発生させなければならないと定め、加入者アカウントごとに少なくとも2つの通知先を保持できるようにすること、通知には心当たりがない場合に備えて連絡先を含む明確な指示を記載することを求めています。ASVS 5.0.0の6.3.7も、資格情報の再設定や利用者名やメールアドレスの変更といった認証情報の更新の後に利用者へ通知することを検証項目にしています。

セッションの寿命と失効の設計は、こちらで詳しく整理しています。

あわせて読みたい

セッション管理とセッションハイジャック対策。Cookie属性とセッションID再生成で不正利用を防ぐ設計

復旧経路をどこに置くかで上限が決まる

再設定フローの強度は、最終的に「どの経路で本人に届けるか」で決まります。経路ごとに前提と弱点が異なるため、比較してから選びます。

復旧経路成立の前提主な弱点適した使いどころ
登録済みメールアドレスメールアカウントが本人の管理下にあるメールが最上位の資格情報になり、受信箱の乗っ取りで連鎖する標準経路。単独で最終手段にしない
SMSまたは音声通話電話番号の契約が本人の管理下にあるSIMスワップや番号ポーティングで奪われる補助的な確認。単独の復旧根拠にはしにくい
保存型の復旧コード利用者が登録時に受け取り保管している保管を忘れる、紛失する、端末に画像で残る多要素認証の利用者へ登録時に配布する
別の認証子での認証パスキーや認証アプリなど別の手段が生きているすべての認証子を同時に失うと使えない認証子を2つ以上登録させる設計と組み合わせる
本人確認のやり直し登録時に身元確認を実施し記録がある実施コストが高く運用が属人化しやすい高い保証が必要なサービスの最終手段
ヘルプデスク経由担当者が本人確認手順を持っている社会的な説得で突破される手順を文書化したうえでの最終手段

NIST SP 800-63B-4は、公衆交換電話網(PSTN)を使う帯域外認証の節で、検証者は帯域外の認証シークレットをPSTNで配信する前に、端末の交換、SIMの変更、番号ポーティング、その他の異常な挙動といったリスク指標を考慮すべきであると述べています。事前登録された電話番号の設定や変更を新しい認証子のバインドとみなし、認証子のバインドの手順に従ってのみ行うことも同じ節の要件です。どちらも復旧コードの節ではなく帯域外認証の節に置かれた要件ですが、SMSや音声で復旧コードを送る設計にも同じ注意がそのまま当てはまるため、援用できます。電話番号の変更画面が再認証なしで素通りする実装になっていないかは、確認する価値があります。

NIST SP 800-63B-4は、アカウント復旧の必要性を最小化するため、加入者に少なくとも2つの独立した認証手段を維持するよう促すべきである(SHOULD)と述べています。また新しい認証子をバインドする際は、加入者アカウントで利用可能な最大の認証保証レベルか、その認証子が使われる最大の認証保証レベルのいずれか低いほうでの認証を要求し、追加時にはバインド処理とは独立した手段で通知することを求めています。

電話番号を復旧経路に据える設計の危うさは、こちらで扱っています。

あわせて読みたい

SIMスワップで電話番号が奪われる仕組みとSMS認証を見直す対策

メールだけに寄せる設計を選ぶなら、その帰結を受け入れる必要があります。メールアカウントを奪われた時点で、そのアドレスだけで再設定できるサービスは連鎖的に奪われます。追加の本人確認や復旧コードを要求する設計であれば、連鎖はそこで止まります。ASVS 5.0.0の6.3.6は、メールを単一要素認証としても多要素認証の要素としても使わないことをレベル3の要件にしています。メールは通知の経路であって認証の要素ではない、という整理です。

パスキーを導入したサービスでは復旧の設計がむしろ難しくなります。FIDO Allianceはパスキーを、クラウド経由で端末間に同期される同期型と、単一の端末から出ない端末バインド型に分けています。同期型は同期先のプラットフォームアカウントに紐づくため、そのアカウントへアクセスできなくなると、そこへ同期されている複数のパスキーをまとめて使えなくなります。端末バインド型は同期の連鎖こそありませんが、その端末を失うと、その端末に入っていたパスキーは戻せません。別の端末や追加の認証子、セキュリティキーのような復旧用の資格情報を登録していなければ、残る経路はメールなどの別手段だけになります。ASVS 5.0.0の6.4.4は、多要素認証の要素が失われた場合、登録時と同じレベルの身元確認の証跡を実施することを求めています。パスキーだけを提供して復旧経路をメールに落とせば、実効的な強度はメールアカウントの強度に戻ります。

パスキーそのものの仕組みは、こちらで整理しています。

あわせて読みたい

パスキーとは。パスワードのない世界へ、仕組みと使い方・移行を解説

NIST SP 800-63B-4が定める復旧の組み合わせ

NIST SP 800-63B-4は2025年7月31日にFinalとして公開され、2017年6月に発行され2020年3月2日に更新されたSP 800-63Bを置き換えました。副題は「Authentication and Authenticator Management」で、以前の版の「Authentication and Lifecycle Management」から変わっています。同文書は復旧方法を、保存型の復旧コード、発行型の復旧コード、復旧用の連絡先の利用、身元確認のやり直しという4つのクラスに整理しています。

注目すべきは、認証保証レベル2(AAL2)での復旧要件が「1つでは足りない」と定められている点です。AAL2まで認証できるアカウントの復旧には、次のいずれかの完了が求められます。1つ目は、保存型、発行型、復旧用連絡先という集合の中から異なる方法で得た2つの復旧コードを使うこと。2つ目は、その集合から得た1つの復旧コードに加えて、そのアカウントにバインドされた単一要素の認証子で認証すること。3つ目は身元確認のやり直しです(アカウントが身元確認済みである場合)。

復旧先アドレスの登録手順にも要件があります。身元確認の過程で検証済みのアドレス以外を復旧先にする場合、復旧コードと同じ性質を持つ確認コードを新しいアドレスへ送り、正しい確認コードが提示されてはじめて復旧先として登録する、と定められています。復旧先を少なくとも2つ登録できるようにすることも求められています。設定画面でアドレスを入力しただけで復旧先が有効になる実装は、この要件を満たしません。

レート制限にも数値の目安があります。同文書は、認証子の説明で特に指定がない限り、単一の加入者アカウント上で特定の認証子に対する連続した認証失敗を100回までに制限し、その認証子を無効化することを求めています。上限が100とされた理由は、6桁の10進のワンタイムパスワードに対する100回の試行で正解を引く確率と、上限到達時のアカウント復旧の必要性を釣り合わせたためと説明されています。正当な利用者が締め出される可能性を下げる手段としては、ボット検出への回答の要求、失敗のたびに待ち時間を増やす方式(30秒から1時間程度)、リスクベース認証が挙げられています。

秘密の質問を認証手段から外す

「母親の旧姓」「初めて飼ったペットの名前」といった秘密の質問は、いまも多くの再設定フローに残っています。NIST SP 800-63B-4は扱いを明確にしています。検証者とCSPは、パスワードを選ぶ際に知識ベース認証(KBA)や秘密の質問を利用者に使わせるよう促してはならない(SHALL NOT)、というものです。例として「What was the name of your first pet?」が挙げられています。同じ箇所には、認証されていない請求者がアクセスできるヒントを加入者に保存させてはならない、という要件も並んでいます。ASVS 5.0.0の6.4.2は、パスワードのヒントやいわゆる秘密の質問が存在しないことをレベル1の検証項目にしています。レベル1は最も緩い水準で、どのアプリケーションでも満たすべき最低線として置かれています。

OWASPのChoosing and Using Security Questions Cheat Sheetは、冒頭の警告で、秘密の質問はNIST SP 800-63で受け入れ可能な認証要素として認められていないと述べ、アカウント復旧は認証の別の方法にすぎないため通常の認証より弱くてはならない、と説明しています。同シートは、安全なソフトウェアに秘密の質問の受け入れ可能な用途はないとしたうえで、既存システムの都合で使う場合の質問の選び方を示しています。

OWASPのForgot Password Cheat Sheetも、秘密の質問は答えが推測しやすいか攻撃者に入手されやすいため、パスワード再設定の唯一の仕組みとして使うべきではないとし、他の方法と組み合わせる場合には追加の防御層を提供しうる、と述べています。ただしASVS 5.0.0の6.4.2は、秘密の質問が存在しないことをレベル1で求めています。追加の要素として新たに採用する選択肢はなく、既存システムに残っている場合も撤去までの経過措置として扱います。CWE-640は緩和策として、誤答回数に絞りをかけ、少ない回数を超えたらパスワード復旧機能を無効化することを挙げていますが、Forgot Password Cheat Sheetは忘れたパスワードへの攻撃でアカウントをロックアウトすべきではないとしています。両者を並べると、止める対象は秘密の質問という手段に限り、アカウント本体や他の復旧経路までは止めない設計に落ち着きます。

人が介在する復旧経路の統制

自動の経路をすべて塞いでも、最後に人が残ります。OWASPのForgot Password Cheat Sheetは、利用者が必ずアカウントを復旧できる手段を持つようにしなければならない、それがサポートチームへの連絡と担当者への本人確認を伴うものであっても、と述べています。人手の経路は消せない以上、統制の対象にします。

OWASPのMultifactor Authentication Cheat Sheetは、多要素認証を失った利用者への復旧方法の候補として、初回設定時に単回使用の復旧コードを渡すこと、複数種類の多要素認証を設定させること、登録済みの住所へ復旧コードやハードウェアトークンを郵送すること、サポートチームによる厳格な本人確認、別の信頼できる利用者による保証を挙げています。そのうえで決定版の方法は存在しないと述べ、全員が互いを知る社内向けアプリケーションで機能する方法が、世界中に数千人の利用者がいる一般向けアプリケーションで機能するとは限らない、と指摘しています。

メモ

ヘルプデスクの手順書には、確認できたときの流れだけでなく、確認できなかったときに何と言って断るかまで書きます。断り方が書かれていないと、担当者は目の前の利用者を助けるために例外を作ります。

管理者による代理操作にも境界を引きます。ASVS 5.0.0の6.4.6は、管理者が利用者のパスワード再設定プロセスを開始できることは認めつつ、それによって管理者が利用者のパスワードを変更したり選んだりできてはならないと定めています。理由として、管理者が利用者のパスワードを知る状況を防ぐためと明記されています。管理画面に「パスワードを直接設定する」ボタンがある実装は、この要件から外れます。

多要素認証の要素を差し替える経路も同じ強度で守ります。OWASPのMultifactor Authentication Cheat Sheetは、既に登録されている要素での再認証を要求すること、有効なセッションだけに依存しないこと(セッションが乗っ取られている可能性があるため)、要素の入れ替えを高リスク操作として扱いリスクベースの確認を適用すること、変更時は帯域外の経路で利用者へ通知することを挙げています。

トークンの実装で決まる安全性

設計要件を実装に落とすとき、判断が必要な箇所は、トークンの保管、単回使用の保証、要求側の処理の一定化、ログに残す範囲の4つに絞られます。

まず保管です。平文保存を避ける方法は2つあります。1つは、提示されたトークンをSHA-256のような決定的なハッシュ関数でダイジェスト化し、そのダイジェストに索引を張って行を引く方法です。ソルトを使わなければ検索キーとして機能するため、1本の値のままでもハッシュ化保管と両立します。もう1つは、トークンを「検索用の識別子(selector)」と「検証用のシークレット(verifier)」に分け、識別子で行を引き、シークレットのハッシュだけを保存して比較する方法です。後者は検証側にソルト付きの関数を使う余地が残り、識別子と秘密の役割が分かれるため扱いやすくなります。必須の設計ではありませんが、以下では後者を例にします。

CREATE TABLE password_reset_tokens (
  id            BIGSERIAL PRIMARY KEY,
  user_id       BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  selector      TEXT NOT NULL UNIQUE,   -- 検索用。秘密ではない
  verifier_hash BYTEA NOT NULL,         -- 検証用シークレットのハッシュ
  expires_at    TIMESTAMPTZ NOT NULL,
  used_at       TIMESTAMPTZ,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

次に単回使用です。「行を読んで、使用済みでなければ更新する」という2段階の処理は、同じリンクが同時に開かれたときに両方が通る余地を残します。検証用シークレットのダイジェストまで条件に含めた1本の条件付き更新にすれば、その余地が消えます。

UPDATE password_reset_tokens
   SET used_at = now()
 WHERE selector = $1
   AND verifier_hash = $2  -- 提示された検証用シークレットのダイジェスト
   AND used_at IS NULL
   AND expires_at > now()
RETURNING user_id;

更新された行が返らなければ、そのトークンは存在しないか、期限切れか、使用済みか、検証用シークレットが誤っています。ダイジェストを条件に含めるところが要点です。selectorだけを条件にしてused_atを先に更新すると、正しいselectorと誤ったシークレットの組み合わせで正規のトークンを消費でき、正当な利用者の復旧を妨げる手口が成立します。ダイジェストどうしの比較は、元の値が十分なエントロピーを持つ乱数であれば、逐次比較であっても当てにいく手がかりを与えません。

アプリケーション側で定数時間の比較関数を使いたい場合は、同じトランザクションの中でSELECT ... FOR UPDATEによって行をロックし、比較に成功したときだけused_atを更新します。OWASPのAuthentication Cheat Sheetは、パスワードの比較関数について、タイミング攻撃から守るために定数時間で返ること、入力長の上限を設けることを挙げています。

消費する場所も決めておきます。メールのリンクを開いたGETの時点では消費せず、期限内で未使用であることの確認と入力フォームの表示にとどめ、新しいパスワードを送信する最後のPOSTでこの更新を実行します。このPOSTでは、トークンの条件付き更新、新しいパスワードの保存、既存セッションの失効を同じトランザクションに入れます。分けて実行すると、トークンだけが使用済みになりパスワードは変わらないままという状態が起こり得ます。GETで消費する実装は、メールクライアントやセキュリティ製品がリンクを先に取得しただけで、利用者が使う前にトークンを失う可能性を抱えます。

要求側は、アカウントの有無で分岐せず同じ手順を通ってから同じ応答を返す形にします。利用者を検索した後の処理はそれ自体が分岐なので、存在確認から送信までを非同期のワーカーへ移し、リクエストの処理には有無で変わる部分を残しません。

import { randomBytes, createHash } from "node:crypto";

const TTL_MINUTES = 30;

function newToken() {
  const selector = randomBytes(12).toString("base64url");
  const verifier = randomBytes(32).toString("base64url");
  return { selector, verifier, hash: createHash("sha256").update(verifier).digest() };
}

// リクエスト側。アカウントの有無で分岐する処理を1つも持たない
export async function requestReset(email: string, ip: string) {
  const normalized = normalize(email);
  await rateLimiter.consume(`reset:email:${normalized}`);
  await rateLimiter.consume(`reset:ip:${ip}`);
  // 存在確認も発行も送信もワーカーへ渡す。積むジョブは常に1件
  await resetQueue.enqueue("password-reset-request", { email: normalized });

  // 応答は常に同一。ログにもトークンは残さない
  return {
    status: 200,
    message: "入力されたアドレスが登録されている場合、再設定用のメールを送信しました",
  };
}

// ワーカー側。ここでの処理時間の差はレスポンスに現れない
export async function handleResetRequest({ email }: { email: string }) {
  const user = await findUserByEmail(email);
  if (!user) return;
  await invalidateActiveTokens(user.id);
  const { selector, verifier, hash } = newToken();
  await saveToken({
    userId: user.id,
    selector,
    verifierHash: hash,
    expiresAt: minutesFromNow(TTL_MINUTES),
  });
  await sendResetMail(user.email, `${TRUSTED_ORIGIN}/reset?s=${selector}&v=${verifier}`);
}

上の例で再設定URLのオリジンを定数にしているのは、Hostヘッダインジェクションを避けるためです。トークンをURLに載せる方式には参照元の漏えいという固有のリスクが伴うため、再設定ページにReferrer-Policyを設定します。値の指定には注意が要ります。W3CのReferrer Policy仕様が列挙する値はno-referrer、same-origin、strict-originなどであって、noreferrerはa要素などのrel属性で使うリンク関係値です。ヘッダやmeta要素に書くのはno-referrerです。IPAの「安全なウェブサイトの作り方」も、セッションIDをURLパラメータに格納しない理由として、ブラウザのReferer送信機能でリンク先へURLが送られる点を挙げており、同じ機構がトークンにも当てはまります。URL全体を記録するアクセスログやプロキシのログにも値が残るため、クエリ文字列を丸ごと書かない設定にするか、URLトークンからPINと同様の限定的なセッションを作る方式を検討します。

ログに残すのは、いつ誰の再設定が要求され、どのIPから、どの結果になったかという事実であって、トークンそのものではありません。OWASPのSession Management Cheat Sheetは、記録すべき事象として、セッションIDの作成、更新、破棄と、ログインとログアウトでの利用、セッション内での権限レベルの変更、セッション中の重要な業務操作を挙げています。パスワード変更の試行と完了については、同シートの別の節で、再認証を要求すべき高リスクな事象として挙げられています。記録すべき事象の列挙は例示であり、再設定の要求と完了を記録してはならないという趣旨ではありません。要求が特定アカウントに集中している、という検知はこのログがなければできません。

再設定フローの検証で落とすテストケース

OWASPのWeb Security Testing Guideは、脆弱なパスワード変更または再設定機能のテストで、管理者以外の利用者が自分以外のアカウントのパスワードを変更または再設定できないか、処理を操作して他の利用者や管理者のパスワードを変えられないか、その処理がCSRFに対して脆弱でないかを確認項目に挙げています。パスワード変更では、完了に古いパスワードが要求されるかも確認します。

パスワード再設定フローの確認項目

  • 登録済みアドレスと未登録アドレスで、画面の文言、HTTPステータスコード、リダイレクト先、応答ボディの長さ、応答時間の分布が一致するか
  • 発行されるトークンが暗号論的に安全な乱数生成器由来で十分な長さを持ち、連番や時刻由来の規則性がないか
  • トークンがデータベースに平文で保存されず、アクセスログや監視ツールのトレースにも記録されていないか
  • 同じトークンを2回使うと2回目が失敗するか。並列に2本同時送信しても片方しか成功しないか。誤った検証用シークレットで試したときに正規のトークンが消費されないか
  • 有効期限を過ぎたトークンが失敗するか。新しいトークンの発行で未使用の旧トークンが失効するか
  • 再設定の完了後に、既存のセッション、リフレッシュトークン、永続ログイン用トークンが無効化されるか
  • 再設定の完了後に通知が送られ、その本文に新しいパスワードが含まれていないか
  • 多要素認証が有効な利用者で、再設定だけで多要素認証を迂回できないか(ASVS 6.4.3)
  • 再設定の要求と検証の両方に、アカウント単位とIP単位のレート制限が効くか。大量に叩いても正当な利用者が締め出されないか
  • 再設定URLがリクエストのHostヘッダから組み立てられていないか。Hostを改ざんした要求で確認する
  • 再設定ページにReferrer-Policy: no-referrerが設定され、外部リソースの参照でトークンが漏れないか
  • メールアドレスや電話番号の変更に再認証が要求され、変更後に旧アドレスへも通知が届くか
  • 管理画面から管理者が利用者のパスワードを直接設定できないか(ASVS 6.4.6)。秘密の質問やパスワードヒントが存在しないか(ASVS 6.4.2)

自動化できる範囲を切り分けておくと運用に載ります。応答の一致、単回使用、期限切れ、レート制限は結合テストで機械的に確認できます。応答時間の分布とReferrer漏えいは計測が要るのでリリース前の手動確認に寄せ、ヘルプデスク経由の復旧は模擬依頼を定期的に流して確かめます。

設計を見直すときに戻る三つの原則

判断に迷ったとき戻れる原則が3つあります。1つ目は、復旧は認証の代替経路であり通常の認証より弱くてはならないという原則です。OWASPのChoosing and Using Security Questions Cheat Sheetが述べるとおり、アカウント復旧は認証の別の方法にすぎません。ASVS 5.0.0の6.4.3は、忘れたパスワードの再設定プロセスが、有効化されている多要素認証機構を迂回しないことを求めています。

2つ目は、再設定に使う値はシークレットであり、パスワードと同じ扱いをするという原則です。暗号論的に安全な乱数から生成し、ハッシュ化して保管し、単回使用にし、短い期限を付けます。NIST SP 800-63B-4が示した上限(SMSまたは音声で10分、電子メールで24時間)は、発行型の復旧コードに対する要件であって再設定トークン全般の規定ではありませんが、期限を決めるときの目安になります。ログやURLに値が残る経路を潰すところまでが、この原則の範囲になります。

3つ目は、復旧経路の強度がサービス全体の強度の上限になるという原則です。メール1本に寄せた設計は、利用者のメールアカウントの強度がそのままサービスの強度になることを意味します。NIST SP 800-63B-4は、AAL2で認証できるアカウントの復旧について、異なる方法で得た2つの復旧コード、1つの復旧コードと単一要素認証子による認証の組み合わせ、身元確認のやり直しのいずれかを求めています。復旧コードを使う経路では、異なる2つの根拠がそろうことを求めている形です。

着手の順番としては、応答の一致とレート制限から始めるのが現実的です。この2つは既存コードへの影響が小さく、ユーザー列挙と総当たりという頻度の高い攻撃を同時に減らします。次にトークンのハッシュ化保管と単回使用の原子的な更新へ進み、最後に復旧経路の見直しと通知の整備に取りかかります。CWE-640が述べるとおり、パスワード復旧の機能は注意深く設計されなければシステムの最も弱い環になります。認証を固める作業は、ログイン画面ではなくこの経路を見たときに終わります。

出典・参考

この記事をシェア

関連する記事