セキュアなフォーム画面の入力体験を考える

背景

保険金を請求するフォーム画面の開発プロジェクトに関わった。
ドメインが保険金の請求のため、扱う情報は機密性が高く、高いセキュリティ要件が必要になるはずだったが、 実際の仕様としては以下の形に落ち着いた。

  • 導線はモバイルアプリからのWebへの画面遷移で、リダイレクトで画面表示する
  • そのような導線のためログイン機能は存在しなかったが、なぜかセッション管理も存在しなかった
  • 初期画面表示ではユーザー情報や請求に必要なIDを取得するためのトークンがURLに付与されてくるので、そのトークンを使用して、請求IDとユーザー情報を取得する
  • 画面入力 -> 入力内容確認画面を経て、請求確定ボタンを押すと、請求IDと入力情報をサーバーに投げて請求完了

上記の通りセキュアでないポイントが多々あったが、バックエンド&インフラは協力会社の方で管理されていた上、こちらから口出しできない&仕様変更の依頼も許可されていない状態のため、ユーザビリティやセキュリティの面で 妥協したポイントがいくつかあった。
これを反面教師反省として活かすため、今回のアプリケーションは本来どのように設計すべきであったかを書き留めておこうと思う。

セッションの必要性

セッションとは何か、再度確認しておく。

Web セッションは、同じユーザーに関連付けられた一連のネットワーク HTTP 要求および応答トランザクションです。
OWASPチートシートより引用

HTTPはステートレスのため、リクエストに対してどのユーザーからの情報なのかをサーバー側で判断する必要がある。 例えば、その判別IDとしてSessionIDをCookieに持っておくなど。

ユーザーを判定したいケースで言えば、ユーザー状態の追跡(ショッピングカートやアプリケーション内でのユーザーの選択など、ユーザー状態を追跡してサーバー側で管理)などがあるが、逆にそういった必要性がない場合は、サーバー側でセッション管理する必要性は薄くなる。

今回のアプリケーションにセッションは必要?

以下2点の理由で必要。

  • フォーム画面のリロード時のユーザービリティ
  • セッションタイムアウト

フォーム画面のリロード時のユーザービリティ

フォーム画面の入力中の情報は、特に何もしなければ画面のリフレッシュによって消滅する。
入力項目の少ない画面だとユーザーに再入力させるで問題なさそうだが、入力項目が多い画面だと再入力のコストが大きくなる。
スマートフォン操作だと誤ってブラウザリロードボタンに指が触れることがあってもおかしくないので、考慮したいところ。

今回のアプリケーションの画面遷移は 入力画面 -> 入力確定画面 -> データSubmit という流れ。
これを見た時に、入力確定画面まで操作を進めた上でデータが消えてしまうのは少なくとも避けたい。
なので、入力確定画面に表示される入力データは、画面リロード後も入力内容は保持されてほしい。

ここで、フォームの入力内容を保持する手段として、以下の方法が考えられる。

  • ブラウザのストレージ(localStorage / sessionStorage)
  • URLのクエリパラメータ
  • サーバーのストレージ

今回は機密性の高い情報を扱うこともあり、ブラウザ側での保持はセキュリティリスクになりうるため、サーバー側でストレージを持つ方向で考える。
ということで、入力確定画面への遷移イベント(入力確定ボタンを押下するなど)にて、サーバーへ入力途中のフォームデータを送信する必要がある。

その際にサーバーでのデータの持ち方として、入力途中データというユーザー固有のデータを、セッションで管理すべきと考える。
セッションIDと紐づく形で入力途中の情報を保持しておけば、セッションが無効になったタイミングで入力情報も破棄することができ、登録されず不要になったゴミデータの削除サイクルを考える必要もない。
画面がリロードされた際は、画面初期描画時にサーバーにセッションIDに紐づく入力画面の存在を確認しに行き、データがあれば入力項目にセットするといった形で、リロード時も情報を復元できる。

※ 実務では、バックエンド側でユーザーセッションを管理するような実装になっていなかったため、リロード時にデータを復元する機能を作ることができず、結局リロードした際はエラー画面に飛ばすといった、ユーザビリティのよろしくない作りとなってしまった。

セッションタイムアウト

機密性の高い情報をユーザーに入力させる場合、セッションタイムアウトを設定することがセキュリティ上望ましいとされる。セッションタイムアウトは、一定時間の無操作状態が続いた場合に、自動的にユーザーのセッションを終了させることができ、これにより、放置された端末からの不正アクセスや情報漏洩のリスクを軽減することができる。

そして、このセッションタイムアウトは原則バックエンド側で実施すべきである。
ブラウザ側(クライアントサイド)のみでタイムアウトを管理する場合、悪意あるユーザーや攻撃者がJavaScriptを無効化したり、ブラウザの開発者ツールを使用してタイムアウトを回避することが可能になるリスクがある。

※ 実務では、クライアント側で時間を計測して、指定の時間経過を検知するとエラー画面に遷移させるといった、擬似タイムアウトを行なっていた。

クライアント側でのSQLインジェクション対策はアンチパターン

JavaScriptにて行う処理は、minifyされていようと基本的にユーザーが書き換えて実行可能なため、SQLインジェクション対策のようなセキュリティに関わる実装をフロントエンドで行うべきではない。
これはSQLインジェクション対策のみならず、入力値バリデーションがバックエンドでも必要な理由と同様である。

バックエンドの仕様変更が許されていなかったものの、バックエンド側でSQLインジェクション対策が実装されていなかった。 そのためフロントエンド側で行わざるを得なかった。