パスワード再設定リンクの不正使用を防ぐ

サイバーセキュリティBeginner
オンラインで実践に進む

はじめに

Harbor の会員ポータルでは、パスワードを忘れた人がアカウントを復旧できます。再設定メールには一時的なトークンが含まれ、その所持によって特定のアカウントのパスワードを一度だけ変更できるはずです。しかしサポート担当は、リンクの再利用や別アカウントの変更が可能だと発見しました。

まず架空のアカウントを通常の手順で復旧し、危険な動作を観察して、小さな Python ハンドラーを修正します。Python の経験は不要です。編集の前に関数や条件を説明します。端末でのコマンド入力とタブの切り替えには慣れていることを前提とします。

アプリは Portal Web、模擬メールは Mailbox Web、準備済みコードは App Server タブを使います。すべて同じ独立した学習用インスタンスで動作し、外部へのメール送信は行いません。トークン、期限比較、トランザクション、パスワード変更、ログインは実際に実行されますが、本番用の本人確認システムではありません。

通常の手順でアカウントを復旧する

このステップでは、再設定メールからパスワードを変更してログインし、修正後も維持すべき正常な動作を確認します。

このステップを始めるには、ラボ画面の Portal Web タブを選択してください。

アカウントは利用者を識別し、パスワードはログインできることを証明します。忘れた場合、再設定リンクが変更を一時的に許可します。リンク内のトークンはパスワードそのものではありません。ローカルメールボックスでは、架空の maya と noah の所有者として操作できます。初期パスワードはいずれも StartPass42! です。この学習用アカウントだけを使用してください。

リンク発行、パスワード再設定、ログインの Portal Web フォーム

Portal Web タブに切り替えます。Request a reset link を見つけ、Recovery account に maya を入力し、Send reset link をクリックします。Link requested. Open Local Mailbox. と表示されます。Mailbox Web タブに切り替えます。再設定フォームや結果が表示されていたら、ページ内の Local Mailbox を選択してメール一覧に戻ります。一覧を更新すると、最新メールが先頭にあり、宛先、Open reset link、トークン文字列が表示されます。

新しい再設定リンクのメールを表示する Mailbox Web

現在の Mailbox Web タブに表示されたリンクとトークンを使ってください。トークンの値は環境ごとに異なります。

そのメールの Open reset link をクリックします。同じタブにフォームが開き、Token と Reset account が入力済みになります。両方を変更せず、New password に FirstPass42! を入力して Reset password をクリックします。Reset accepted と HTTP 200 が期待されます。

Mailbox Web タブ内に表示される再設定の成功結果

HTTP はブラウザーとサーバーが要求と応答をやり取りする仕組みです。ここでは 200 は受理、403 は拒否、500 はアプリケーションエラーです。接続エラーはセキュリティ上の拒否とは異なります。

Portal Web タブに切り替えます。リンク発行の結果が表示されていたら、ページ内の Back to portal を選択してフォームを開き直します。Sign in で Login account を maya、Password を FirstPass42! にして Sign in をクリックします。Login accepted が期待されます。正常な再設定ではページの表示だけでなく、実際のパスワードが変更されます。

リンクをなくしたら同じフォームで再発行し、最新メールを使用してください。有効期間は 30 分です。期限切れや入力ミスのために環境を再起動する必要はありません。

壊れた境界を観察する

このステップでは、使い捨てのデモ変更でトークンの所有者、有効期限、一度限りの権限を区別します。

このステップを始めるには、ラボ画面の Portal Web タブを選択してください。

ブラウザーは名前と値を持つフィールドを送信します。利用者が変更できるため、サーバーはメールが自動入力したというだけで Reset account を信頼できません。サーバーには別のトークン記録があります。

フィールド 意味
account このトークンを使用できるアカウント
expires_at 使用可能な最終時刻。固定の時点からの秒数
used 使用前は 0、使用後は 1

新しいトークンは、期限前に所有者だけが使えます。再設定に成功すると使用済みになり、再び使える状態に戻ってはいけません。未使用のまま時間経過で期限切れにもなります。使用済み、期限切れのどちらも次の変更を許可しません。

一つのリンクを再利用する

Portal Web タブに切り替え、結果が表示されていたら Back to portal を選択して、maya の新しいリンクを発行します。Mailbox Web タブに切り替えます。再設定フォームや結果が表示されていたら Local Mailbox を選択してメール一覧に戻ります。一覧を更新して最新リンクを開き、maya のパスワードを DemoPass42! に変更します。Mailbox Web タブの結果ページで Local Mailbox を選択してメール一覧に戻ります。同じメールのリンクを再び開き、maya のパスワードを ReplayDemo42! に変更します。両方が Reset accepted になります。二度目は拒否されるべきです。これはリプレイ、つまり使い切ったはずの権限の再利用です。

別のアカウントを試す

Mailbox Web タブで Local Mailbox を選択し、同じメールのリンクを再び開きます。Reset account を noah、New password を CrossDemo42! に変えて Reset password をクリックすると受理されます。Portal Web タブに切り替え、Back to portal を選択してログインフォームを開き直します。noah と CrossDemo42! でログインします。maya 用リンクで noah が変更されました。サーバーがトークンをアカウントに結び付けていません。

期限切れのトークンを試す

Portal Web タブのまま Back to portal を選択し、再設定フォームを開き直します。Token に expired-demo、Reset account に maya、New password に ExpiredDemo42! を入力します。この準備済みトークンは環境準備の 1 時間前に期限切れです。Reset password をクリックすると、これも受理されます。サーバーが時刻を無視しているのです。タイマーを待つ必要はありません。

これらは使い捨てのデモ用アカウントで、修正テストには新しいコピーを使います。観察した状態を維持し、データベースを直接編集する必要はありません。

トークンを結び付けて消費する

このステップでは、小さなサーバー関数を修正し、稼働中のデモアカウントを変えずにテストします。

このステップを始めるには、ラボ画面の App Server タブを選択してください。

App Server タブに切り替えます。開始ディレクトリは /home/labex/project/reset です。cat は引数のファイル名にあるテキストを表示します。ハンドラーを読みます。

次のコマンドを実行する前に、App Server タブに切り替えてください。

cat handler.py

最初の行は準備済みの補助関数を二つ読み込みます。def は関数を定義し、丸括弧内の名前はパラメーター、つまり一回の呼び出しへの入力です。eligible(row, account, now) は保存されたトークン記録、送信されたアカウント、現在のサーバー時刻を受け取ります。row["account"] は記録のフィールドを読みます。row is None はトークンが見つからないことを表します。字下げされた if の本体は条件が成立したときだけ実行されます。return False は権限を拒否し、return True は許可します。ここでは先頭の四つの空白が Python のブロックを作ります。

現状は存在するすべてのトークンで True を返すため、三つの問題が起こります。reset も、トークンを消費せずにパスワードだけ変える unsafe_reset を呼び出しています。

修正にはすべての条件が必要です。== は値の比較、> は大きいこと、and は両側が真であることを要求します。or だと片側だけで許可され、一つの正しい属性が別の不正な属性を隠してしまいます。将来の期限は now より大きく、同じ時刻ならすでに期限切れです。used == 0 は未使用だけを許可します。

consume_and_reset は SQLite トランザクションを開始し、現在のトークンを読み、あなたの eligible を呼び出し、パスワード変更と used = 1 を一緒に確定します。これが原子性、つまり変更が一緒に成功する性質です。読み取り前にロックするため、競合する二つの要求が両方とも未使用状態を見ることを防ぎます。ロックやパスワードのハッシュ処理を実装する必要はありません。

端末用テキストエディター nano でファイルを開きます。

nano handler.py

内容を以下のコードに置き換えます。矢印キーと Backspace/Delete で古い内容を消し、全体を貼り付けられます。空白と記号を正確に保ってください。丸括弧によって長い条件を複数行に分けられます。

from framework import unsafe_reset, consume_and_reset


def eligible(row, account, now):
    if row is None:
        return False
    return (row["account"] == account
            and row["expires_at"] > now
            and row["used"] == 0)


def reset(db, token, account, password):
    return consume_and_reset(db, token, account, password, eligible)

Ctrl+O で保存し、Enter で handler.py を確認、Ctrl+X で終了します。各 Web 要求が保存済みコードを読むのでサービス再起動は不要です。HTTP 500 が出たら開き直し、字下げ、括弧、引用符を比較してください。

python3 は Python プログラムを実行します。準備済みの検査プログラムはハンドラーのパスを受け取り、一時 SQLite データで独立した HTTP アプリを起動します。実コードを修正せずにテストし、稼働中のアカウントも変えません。

python3 /opt/reset-lab/check.py handler.py

valid reset accepted、replay denied、competing use has exactly one winner を含む十一行の PASS が期待されます。不安全な編集では FAIL が問題の属性を示します。すべて拒否すると正常な再設定とログインのテストに失敗します。ブラウザーの値や稼働中の DB を変えても、新しいデータで試すコードは修正されません。必要なら nano handler.py を開き直し、修正を保存して同じ検査を繰り返してください。

復旧が引き続き機能することを証明する

このステップでは、修正した稼働サービスを使い、隔離した安全性テストとの関係を確認します。

このステップを始めるには、ラボ画面の Portal Web タブを選択してください。

新しいリンクを一度使う

Portal Web タブに切り替え、結果が表示されていたら Back to portal を選択して、maya の新しいリンクを発行します。Mailbox Web タブに切り替えます。再設定フォームや結果が表示されていたら Local Mailbox を選択してメール一覧に戻ります。一覧を更新して最新リンクを開き、maya のパスワードを ResetSafe42! に変更します。Reset accepted と HTTP 200 が期待されます。Mailbox Web タブで Local Mailbox を選択してメール一覧に戻り、同じメールのリンクを再び開いて maya に ReplayAgain42! を試します。Reset denied と HTTP 403 が期待されます。

ハンドラー修正後に再設定リンクの再利用が拒否される結果

所有者の未使用リンクを維持する

Portal Web タブに切り替え、結果ページが表示されていたら Back to portal を使い、別の新しい maya リンクを発行します。Mailbox Web タブに切り替え、Local Mailbox を選択して更新し、最新リンクを開きます。Reset account を noah に変えて BlockedCross42! を試します。Reset denied が期待されます。この拒否で所有者の未使用リンクが消費されてはいけません。Mailbox Web タブで Local Mailbox を選択して同じメールを開き直し、Reset account を maya のままにして ResetSafe42! を設定すると成功します。

期限切れを拒否し、ログインを維持する

Mailbox Web タブで Back to portal を選択して再設定フォームを開きます。Token expired-demo、Reset account maya、New password BlockedExpired42! を試すと Reset denied が期待されます。次に Portal Web タブに切り替え、Back to portal を選択してログインフォームを開き直します。maya と ResetSafe42! でログインします。Login accepted が必要です。失敗した要求はパスワードを変えていません。noah もテストする場合は、再び Back to portal を選択し、デモ用パスワード CrossDemo42! でログインします。修正後のエンドポイントは無関係な既存アカウントを保持します。

App Server タブで使い捨てデータによる回帰検査を繰り返します。

次のコマンドを実行する前に、App Server タブに切り替えてください。

python3 /opt/reset-lab/check.py handler.py

同時に二回使って一回だけ成功するケースを含め、十一項目すべて成功するはずです。繰り返しても稼働中の記録は変わりません。復旧の許可と悪用の拒否を両方満たす必要があり、ページの停止や常時拒否は適切な修正ではありません。

このアプリは配送を模擬し、本人確認を簡略化しています。本番では安全なメール配送、頻度制限、セッション管理、パスワードポリシーも必要ですが、この修正の対象外です。OWASP の指針 が広い背景を説明しています。

まとめ

実際の再設定を行い、リプレイ、別アカウントへの変更、期限切れの問題を示して、小さな Python ハンドラーを修正しました。保存された所有者、将来の期限、未使用状態、原子的な消費が連携します。使い捨ての HTTP テストで正常な復旧、悪用拒否、他アカウントの保持、競合時の一回だけの成功を証明しました。スタッフポータルのチャレンジでは別の不完全な条件に同じ規則を適用します。