スパム症候群 〜その発病及び傾向と対策に関する一考察 (3:ようやく完)

…なんか、長々と引っ張りすぎてだんだんどうでもよくなってきたんだが(笑)。しかしまあ、これであれこれ調べ物をしたおかげで、多少はWebアプリケーションの脆弱性あたりの知識がつき、結果的にこの間の情報処理技術者試験に役立ったと言えなくもないので、とりあえず最後まで書こう。

設計上の問題点

前回の最後のところで、「問題文とその解答は同じでも、トークン文字列はアクセス毎に異なるようにする」と書いたが、これを実現するためにどうしたかというと、
  • アクセス元のIPアドレスを取得
  • このIPアドレス文字列と、問題の解答文字列と、「プログラム内部で定義したとある文字列」を一定の法則に従ってシャッフル
  • そのシャッフル文字列をさらにハッシュ関数により変換
…という手を使ってトークン文字列としたのである。ここが微妙に誤魔化しているところで、本来「アクセス毎」にトークン文字列を変えるところを、「アクセスIP毎」に変えることで済ませてしまった。つまり、同一IPからのアクセスであれば、同一の問題文に関してはトークン文字列はいつも一緒。なぜこうしたかというと、1回目(書き込みページ表示)、2回目(書き込み実行)の両アクセスで取得でき、しかも両アクセスで内容が変わらない文字列としてはIPアドレスが一番取得するのが楽だったから。CSRF攻撃に対する防御力、という観点だけならこれで問題はなかったので、ついつい易きに流れたわけだが、この安直さが完成間際になって致命的な問題として発覚。つまるところ、IPアドレスが「両アクセスで内容が変わらない文字列」なんて全くの嘘っぱちだったということである。
PCによるネットワーク接続のように、はじめにルータによって接続を確立し、その時点でIPアドレスが付与されるタイプの接続方法であれば、IPが変わらないというのは確かに間違っていないんだが、携帯電話による接続だとそうはならず、1回のリクエスト毎にIPが変わってしまうらしい。正直、この点は自分で携帯から書き込んでみるまで全く思いつかなかったんだが、iモードの通信中に1台の携帯に同一のIPが継続して確保され続けるなんてありえんだろJK、というのは言われてみれば誠に尤もなことであり、これはとんだ盲点だった。

かくして、設計→実装→単体試験→結合試験→実機配備→最終動作確認、と最後の最後まで進んだところでの不具合発覚により設計段階まで戻るという、およそシステム開発としてはこの上なくマズい体たらくな手戻りが発覚したのであった。

設計見直し

基本的に、Webアプリケーションの処理というのは、webブラウザからの1回のリクエストと、それに対するサーバからのレスポンスで完結するものであり、複数のページ間で何らかの情報を共有するためには、サーバ側で情報を継続して保持し、webブラウザからの各アクセスがどのユーザからのものなのかを判別した上で保持した情報を使って処理を行うことが必要となる。この、サーバ側で保持する情報は一般に「セッション情報」などと呼ばれ、複雑なwebアプリケーションを作るための定石である。
今回の掲示板の例でこのセッション情報の仕組みを利用する場合は、
  1. あるユーザから書き込みページ表示要求のアクセスが発生
    1. サーバは(他のアクセスと重複しない)IDを発行する。これを一般にセッションIDなどと呼ぶ。
    2. 書き込み実行時の認証用問題と、その解答文字列を生成する。
    3. セッションIDと、問題解答文字列を紐付けて、サーバ内に何らかの形で保存する。これがセッション情報。
    4. webブラウザへ、問題文を含めた書き込みページのhtmlデータを返す。このとき、一緒にセッションIDを埋め込んでおく。
  2. 返ってきた書き込みページに、掲示板に書き込む文章と問題文の解答を入力した上で、書き込み実行。このとき、埋め込んであったセッションIDも一緒に送られる。
    1. サーバは、埋め込んであったセッションIDを元に、サーバに保存してあったセッション情報を探し出す。
    2. このセッション情報に問題文の解答が入っているので、webブラウザから送られてきた解答と突き合わせ、一致した場合は認証OK。書き込み処理を実行する。
…というのが妥当な考え方。これにより、前に書いたようなIPアドレスを使うことなしに認証処理ができるというわけ。
いや、こんなもんは作る前から知ってましたよ。いやまぢで。私も一応システムエンジニアの端くれですから。じゃあなぜそうしなかったのかというと、要するにPerlだとセッション情報管理がなんかめんどくさそうと思ってしまったから。サーバ側に情報(というか要するにファイル)を残す形にしておくと、それをどのタイミングで消すか、とかその辺の「後始末」がやっかいだな、と考えてしまったのですよ。Ruby on Railsだとこの辺勝手にやってくれるんだけどな。いや、どうやらCGI::Sessionという、この辺の処理を受け持ってくれそうなライブラリがPerlにあるらしい、というのは事前に知ってはいたんだが、使い方をうまく説明してくれる、というかうまく理解させてくれるサイトが見つからなかったので、セッション情報を利用する案は却下してしまった。別にIPアドレス使えばいいじゃん、とかそういうノリで。思えばここが浅はかだった。
ちなみに、このセッション情報を使う仕組みを採用した場合、携帯電話のようにアクセス毎にIPが変わる場合でも書き込み可能になるという他に、今回の改良で採用した「ハッシュ関数による変換」そのものが不要になる、という利点もある。どうも今回の改修で掲示板のレスポンスが悪くなったのは、このハッシュ関数を実行しているせいではなかろうかとにらんでいるので、いいことずくめ、と言える。

今回の教訓

  • 急いては事をし損じる
  • 急がば回れ
  • 先人の知恵は尊重すべし
  • ひらめきは一晩寝かせて考え直せ

                    • -

以上、ようやく終わり。肝心の掲示板の修正だが、まあそのうちぼちぼち。とりあえず携帯からも書き込むほど熱心なユーザはあまりいなさそうなので、モチベーション低め。年末に帰省するときには携帯で書き込みたい場合もありそう(自分が)なので、それまでには何とかしたい。ていうかPCからも書く人少ないしな。アクセスログを見てると、書き込みページを何回もリロードして問題文だけ見てるだろう人は結構いるんだけども。そこまでやったらもうひとこえ、書き込みもしておくれでないかい(笑)。