再設定リンクの境界を修正する

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

はじめに

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

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

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

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

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

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

Portal Web の Request a reset link で、Recovery account に maya を入力し、Send reset link をクリックします。Link requested. Open Local Mailbox. と表示されます。Mailbox Web を更新すると、最新のメールが先頭にあり、宛先、Open reset link、トークン文字列が表示されます。

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

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

Portal Web を開き、必要なら更新します。Sign in で Login account を maya、Password を FirstPass42! にして Sign in をクリックします。Login accepted が期待されます。正常な再設定ではページの表示だけでなく、実際のパスワードが変更されます。

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

壊れた境界を観察する

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

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

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

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

Portal Web で maya の新しいリンクを発行します。Mailbox Web を更新して最新メールを開き、DemoPass42! に変更します。同じメールのリンクを再度開いて ReplayDemo42! に変更します。両方が Reset accepted になります。二度目は拒否されるべきです。これはリプレイ、つまり使い切ったはずの権限の再利用です。

同じリンクを再び開きます。Reset account を noah、New password を CrossDemo42! に変えて Reset password をクリックすると受理されます。Portal Web で noah と CrossDemo42! でログインします。maya 用リンクで noah が変更されました。トークンとアカウントの結び付けが不足しています。

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

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

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

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

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

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 で maya の新しいリンクを発行し、Mailbox Web を更新して最新メールを開き、ResetSafe42! に変更します。Reset accepted と HTTP 200 が期待されます。同じメールを再び開き、maya に ReplayAgain42! を試すと Reset denied と HTTP 403 になります。

別の新しい maya リンクを発行し、Reset account を noah に変えて BlockedCross42! を試します。拒否されるはずです。この拒否で所有者の未使用リンクが消費されてはいけません。同じメールを再度開き、maya のまま ResetSafe42! を設定すると成功します。

フォームで Token expired-demo、Reset account maya、New password BlockedExpired42! を試すと拒否されます。次に Portal Web で maya と ResetSafe42! でログインします。Login accepted は失敗した要求がパスワードを変えなかったことを示します。noah もデモ用の CrossDemo42! でログインできます。無関係なアカウントは保持されます。

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

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

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

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

まとめ

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