セッションを使ったフォーム処理にありがちな問題点

入力画面が複数に渡るフォームで、ユーザが各画面で入力したデータを、hiddenによって引き回すのではなく、セッション変数(セッションIDにヒモ付いてサーバ側に格納される情報)に一時保存するタイプのWebアプリケーションが増えているように思います。

フォーム処理でセッションが使われるのは、「実装をシンプルにしたい」「携帯サイトなどで通信量を減らしたい」といった理由のほかに、「アプリケーションをセキュアにしたい(hiddenだと改竄される)」という理由があるのではないかと思います。

しかし、セキュリティ上の問題は、セッションを使ったフォーム処理においてもしばしば見つかります。

以下では、セッションを使ったフォーム処理で、割と多く見つかる問題について書きます。

画面遷移の不正

ある会員制サイトの、会員登録フォームを会話風に書いてみます。

会員登録の際には、氏名、生年月日、住所の3つを登録します。ここでは、3つとも必須項目であるとします。

まずは正常系です。

(客は店に入り整理番号を受取る)

【窓口1】
客 :整理番号XXX、氏名は山田花子です。
係員:窓口2で生年月日を伝えてください。
   (係員はメモに氏名を書き込む)

【窓口2】
客 :整理番号XXX、生年月日は1950年1月1日です。
係員:窓口3で住所を伝えてください。
   (係員はメモに生年月日を書き込む)

【窓口3】
客 :整理番号XXX、住所は東京都○○です。
係員:山田花子さん、1950年1月2日生まれ、住所は東京都○○で
   会員登録してよければ、窓口4で登録する旨を伝えてください。
   (係員はメモに住所を書き込む)

【窓口4】
客 :整理番号XXXです。会員登録してください。
係員:会員登録しました。ありがとうございました。
   (係員は会員台帳にメモの情報を写す)

窓口では客と係員が会話をしています。客がHTTPクライアントであり、その発言がHTTPのリクエストです。係員はそれを受けて客に返答しています。これは係員=WebアプリケーションがレスポンスのHTMLを返している様子を表していると考えてください。

その他、整理番号はセッションID、メモはセッション変数、台帳はDB(データベース)をそれぞれ表しています。

ここで、どのような不正が可能か考えてみます。

飛ばし

1つ目の不正は処理の「飛ばし」です。例えば、窓口2を飛ばしてみます。

【窓口1】
客 :整理番号XXX、氏名は山田花子です。
係員:窓口2で生年月日を伝えてください。
   (係員はメモに氏名を書き込む)

《窓口2には行かずに、窓口3へ》

【窓口3】
客 :整理番号XXX、住所は東京都○○です。
係員:山田花子さん、-年-月-日生まれ、住所は東京都○○で
   会員登録してよければ、窓口4で登録する旨を伝えてください。
   (係員はメモに住所を書き込む)

【窓口4】
客 :整理番号XXXです。会員登録してください。
係員:会員登録しました。ありがとうございました。
   (係員は会員台帳にメモの情報を写す)

客は、窓口1の係員の「窓口2に行け」という指示を無視して窓口3に行っています。

もし、これが本物の人間がいる窓口であれば、窓口3か窓口4の係員が異常に気付いて登録を完了することはできないのでしょう。

しかし、多くのWebアプリケーションには、そのような異常に気付くためのチェック処理が実装されていません。このようなアプリケーションでは、窓口4に相当する処理では、セッション変数の値を(何もチェックせずに)DBに書き込むだけのことしかしません。窓口3の処理では、住所をチェックするだけです。

実際のところ、このような「飛ばし」によって、一部の項目の入力がないまま最終の登録処理が完了してしまうWebアプリケーションは少なくありません。極端な例だと、途中を全て飛ばして、いきなり最後のリクエストを飛ばしても、登録処理を実行する(しようとする)アプリケーションもあります。

汚染

客は今度は違うタイプのズルをします。窓口2でデタラメな生年月日を伝えます。

(略)

【窓口2】
客 :整理番号XXX、生年月日はXYZ年@!月50日です。
係員:生年月日が不正です。生年月日を教えて下さい。
   (係員はメモに生年月日を書き込む)

《無視して窓口3に行く》

【窓口3】
客 :整理番号XXX、住所は東京都○○です。
係員:山田花子さん、XYZ年@!月50日生まれ、住所は東京都○○です。
   会員登録してよければ、窓口4で登録する旨を伝えてください。
   (係員はメモに住所を書き込む)

(略)

窓口2で、デタラメな生年月日を伝えられた係員は、生年月日を問い返しています。つまり、生年月日を再入力するためのフォームを含むHTMLをレスポンスします。このフォームのサブミット先は窓口2です。

しかし、客はそれを無視して次の窓口3に対して住所をサブミットしています。

ミソは、申告された生年月日が不正であるにもかかわらず、窓口2の係員がメモに生年月日を書き込んでいる(つまりセッション変数に生年月日を記録している)ところです。

フレームワークのセッションスコープのフォームを使用している場合、このような方法でセッション変数を「汚染」できることがあります*1

客がこのまま窓口4に行くとどうなるでしょうか。もし窓口4でセッション変数の値をチェックなしでDBに書き込むのであれば、本来は入力値のチェックではじかれるべき生年月日の値で会員登録処理が完了してしまうでしょう。

(参考)あっちに行って、そっちに行ってから、こっちに行くと不正なデータが登録出来る - masaのメモ置き場

すりかえ

今度は、ショッピングサイトの購入フォームを取り上げてみます。

まずは正常系です。

【窓口1】
客 :整理番号XXX、みかんを10個ください。
係員:窓口2で氏名と住所を伝えてください。
   (係員はメモに商品IDと個数を書き込む)

【窓口2】
客 :整理番号XXX、氏名は山田花子、住所は東京都○○です。
係員:みかん10個で1000円、送料200円で合計1200円です。
   購入してよければ、窓口3で購入する旨を伝えてください。
   (係員はメモに氏名、住所、送料、合計金額を書き込む)

【窓口3】
客 :整理番号XXX、それで購入します。
係員:100円のみかん10個、送料200円、合計1200円です。
   お買い上げありがとうございました。
   (係員は注文台帳にメモの情報を写す)

次は、ずるをするパターンです。

【窓口1】
客 :整理番号XXX、みかんを10個ください。
係員:窓口2で氏名と住所を伝えてください。
   (係員はメモに商品IDと個数を書き込む)

【窓口2】
客 :整理番号XXX、氏名は山田花子、住所は東京都○○です。
係員:100円のみかん10個、送料200円で合計1200円です。
   購入してよければ、窓口3で購入する旨を伝えてください。
   (係員はメモに氏名、住所、送料、合計金額を書き込む)

《無視して窓口1に行く》

【窓口1】
客 :整理番号XXX、高級メロンを10個ください。
係員:窓口2で氏名と住所を伝えてください。
   (係員はメモの商品IDと個数を上書きする)

《無視して窓口3に行く》

【窓口3】
客 :整理番号XXX、それで購入します。
係員:10000円の高級メロン10個、送料200円、合計1200円です。
   お買い上げありがとうございました。
   (係員は注文台帳にメモの情報を写す)

このアプリケーションでは、窓口2で送料や合計金額を計算し、確認のために客に提示すると共に、これらをセッション変数に書き込んでいます。

客は、窓口2に行った後に、窓口1に行き商品をすりかえています。この時点で、セッション変数内の商品と合計金額とは整合していません。この不整合は、客が係員の指示通りに窓口2に行くことで解消されるため、通常は問題になりません。

しかし、客は窓口2に行けという係員の指示にそむいて、商品と合計金額が整合しない状態のまま、窓口3で注文を完了させています。窓口3で何らかの保護機能が働かない場合、本来よりも安い価格で高級メロンが買えてしまうでしょう。

原因・対策など

このような不正な画面遷移を利用した攻撃に対しては、以下のような対策が考えられます。

画面遷移を制御する

一つは、不正な画面遷移を起こさせないようにするために、画面遷移を制御する方法です。実装としては、①セッション変数内に、次に行ってもよい窓口のリストを保持しておく、②全ての窓口ではユーザが来た際にリストをチェックする、③チェックで不正だった場合はエラー処理を行なう――ようなイメージです。つまりある種の「状態管理」を行なうのです。

しばしば、「HTTPはステートレスで、セッションを使うとステートフルになる」というようなことが言われます。しかし、セッションを使ってさえいれば、魔法のように自動で状態が管理されるようになるわけではありません。セッション機構自体は言語やフレームワークが提供してくれますが、その上での状態を定義しそれを管理するのは、個々のアプリケーションの仕事です。

しかし、画面遷移の制御の実装は割と大変です。大抵のフォームには、「最初の入力画面に戻る」のようなボタンがあるわけですが、そのような遷移を含めて、漏れなく「次に行ってよい窓口リスト」を作らなければなりません。当然ながら、リストには過剰な遷移が含まれていてもダメです。

また、ブラウザの戻るボタンや再読込みボタンにどのように対処するか、というのも考えなければなりません。ブラウザの戻る操作などをエラーにするのであれば、実装は比較的簡単ですが、ユーザの使い勝手は下がります。逆に、ブラウザの戻る操作などを許容するならば、実装は少々複雑になります。

最後にデータを検証する

もう一つは、最後にDBに書き込む窓口(リクエスト)処理にて、セッション変数を再度検証する方法があります。

hiddenを使ったフォームでは、hiddenの改竄対策として、最後の窓口で全データを検証してやりますが、セッションを使ったフォームでもそれと同じことをしてやればよいのです。検証をして、値の抜けや異常、不整合を発見したら、単純にエラーとしてやればよいでしょう。

この方法の、画面遷移を制御する方法と比べた際の利点は、簡潔であり、また自然な方法であることです。自然な方法であるということの意味は、そもそもの問題が何かを考えてみれば判ります。

そもそもの問題は、「フォーム処理において不正なデータが登録されてしまうこと」です。不正な画面遷移そのものではなく、不正なデータが登録されることが問題なわけです。ですから、画面遷移に着目してこれを制御しようとするアプローチよりも、データに着目するアプローチの方が自然といえるわけです*2

その他の問題

上記以外の、セッション変数を使ったフォーム処理でよく見られる問題です。

Session Fixation

高木浩光@自宅の日記 - ログイン前Session Fixationをどうするか

に書いてあります。実際のアプリケーションでも、この手の脆弱性を持つものは珍しくありません。

複数ウィンドウ起動時の問題

ユーザが複数のウィンドウを起動することを全く想定していないアプリケーションもあります。

そのようなアプリケーションでは、ユーザが複数のウィンドウを起動していると、ユーザの意図とは異なるデータが登録されてしまったりします。

例えば、ユーザが高級メロンの購入確認画面を開いた後に、別のウィンドウでみかんの購入確認画面を開き、高級メロンの方のウィンドウで「購入確定」ボタンを押下するとします。この場合、ユーザは高級メロンの購入を期待しているのにもかかわらず、実際にはみかんが購入されてしまったりします。

何故このようなことが起こるのかは、先のショッピングサイトの窓口3のやり取りを見ればわかります。

【窓口3】
客 :整理番号XXX、それで購入します。
係員:100円のみかん10個、送料200円、合計1200円です。

念のため、この部分の実際のリクエストデータは、action=commitのようなものであることが多いです(CSRFや二重送信防止用トークンが付くこともあります)。

ユーザが複数のウィンドウを起動している状況では、「それで購入します」の「それ」が何を指しているか明確ではありません。そのために、上記のような意図しない処理が行なわれてしまうことがあります。

このような問題に対しては、様々な対処方法がありますが、いずれの方法でも確認画面のhiddenに何らかのデータを埋め込むことになるでしょう。サービス上の要件によって異なりますが、hiddenには商品・数量・配送先などの注文データ自体であったり、注文データを識別するためのIDやタイムスタンプなどを入れてやります。

まとめ

複数画面のフォームで、ユーザが入力したデータを一時保存する場所として、hiddenとセッション変数の2つがあります。

セキュリティの観点で、hiddenとセッション変数のどちらかが絶対的に優れているということはありません。どちらでも安全なアプリケーションを作れますし、そうでないアプリケーションも作れます。

私が問題だと思うのは、「hiddenの改竄」はWebアプリケーションの開発者の人たちに割と認知されているようですが、セッション変数を使う場合に生じうることは、一般に余り認知されていないように思われることです。

この種の問題は決して新しいものではないのですが、余りまとまって書いた資料が無いようなので、今回日記にしてみました。

*1:汚染という言い方をしているのは、「セッション変数は守られた領域であり、何かしら正当なデータしか入っていない」という前提(思い込み)があると思われるからです。

*2:かといって、画面遷移を制御することに意味が無い訳ではありません。画面遷移を制御すると、一般論として、アプリケーションに存在する脆弱性を攻略したり、そもそも脆弱性を発見するのが多少なりとも難しくなります。