1084 文字
5 分
Webアプリケーションの認証ロジック
Programming2008-12-03


くぅ。またくだらないミスを犯してしまった。。。
細かいことですが、手軽な実装で大きなリスクを減らせる方法を紹介します。
システムの背景
- Webアプリケーション
- HTTPセッション管理をファイルに保存している
- データベースが開発環境と運用環境に分離されている
- 運用環境のデータを開発環境に取り込み、動作テストやリハーサルを行う場合がある
- ユーザのHTTPリクエストによって実行されるプログラムからメールを送信する
- ログイン認証をユーザIDなどの1つの値を保持するのみで実装してある
背景となるアプリケーションアーキテクチャの例です。プログラムの接続先は開発環境・運用環境どちらのデータベースにも切り替えられます。
graph LR A[開発環境データベース] --> C[プログラム] B[運用環境データベース] --> C C --> D[ユーザ]
何が起きたか
上記の背景では、オペレーションミスにより、運用環境に登録されているユーザへメールが送信されてしまう場合があります。
- 開発環境のデータベースを利用する状態でログインする。
- データベースの接続先を運用環境のデータベースに変更する。
- ブラウザで再度アクセスすると、運用環境のユーザIDでログインされた状態になる。
- メール送信を行うプログラムへアクセスすると、運用環境に登録されたユーザへメールが送信される。
セッションには「ユーザID」しか入っていないため、接続先データベースが切り替わっても同じIDを持つ別人としてログインが継続してしまうのが原因です。
対策:ログインの定義を変更する
ログインの定義が「ユーザIDの保持」だと問題が発生するので、「ユーザIDと、そのユーザに紐づく情報の一致」とすることでセッションと実データの不整合を検出できます。関連づける情報を増やすほど、セッションハイジャックの危険性も減らせます。
元の認証ロジックの式は以下です。左辺と右辺が一致した場合はログイン認証を認めます。
前提:
getFromSession()はセッションからログイン情報として保存してある文字列を取得する関数。idはセッションから取得した顧客ID。isValid(id)は顧客IDを引数とし、ログイン可能な状態ならばidを返却する。
getFromSession() == isValid(id)問題を回避するための認証ロジックの式は以下です。
前提:
other1,other2はidに関連した情報。ユーザ登録時から一切変更されない情報で、idを元にデータベースから取得する。hash()は引数の値をseedに取るハッシュ関数。
getFromSession() == hash(isValid(id), other1, [other2...])セッションに保存した時点のデータベースと、検証時のデータベースが異なればother1などの値が一致しなくなるため、環境をまたいだセッションは自動的に無効になります。
現在の視点で補足
この記事は2008年に書いたものですが、「セッションがどの環境のデータを指しているか」という問題自体は今も変わっていません。現在なら以下のような対策を組み合わせるのが一般的です。
- 環境ごとにセッション用の秘密鍵を分ける: RailsのsecretキーやDjangoの
SECRET_KEYのように、セッションの署名・暗号化に使う鍵を環境ごとに変えておけば、環境をまたいだセッションはそもそも復号できず無効になります。 - 本番データを開発環境に持ち込むときはマスキングする: メールアドレスや氏名などの個人情報をダミー値に書き換えてから取り込めば、誤送信しても実ユーザには届きません。
- 開発環境からメールを実送信しない: 開発環境ではSMTPの代わりにMailtrapのようなサンドボックスや、Railsのletter_openerのようなローカル確認ツールに送信先を固定しておくと、アプリケーションのバグやオペレーションミスがあっても被害が出ません。
アプリケーション側の認証ロジックで守るだけでなく、「そもそも開発環境から本番ユーザへメールが届く経路をなくす」方向で多層防御するのが現在の定石です。
関連記事
この記事が役に立ったら
GitHub Sponsorsで応援できます


