出張に向かう電車の中で「9784822284220」を読んでいたんですが、第5章のsectionタグとh1〜h6による見出しとの関係に強い違和感が…。
例えば、Wikiスタイルで
! 見出し1 ああああ !! 見出し2 いいいい
という記述は、
<h1>見出し1</h1> <p>ああああ</p> <h2>見出し2</h2> <p>いいいい</p>
のように変換されますが、これはこのままでHTML5としても正しいようです。ただ、内部的(?)には見出しのレベルを変更すると暗黙の<section>が生成され、上記のソースは
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <h2>見出し2</h2> <p>いいいい</p> </section> </section>
と等価になり、さらにこれは、
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <h1>見出し2</h1> <p>いいいい</p> </section> </section>
とも等価になるんだそうです。この例だけ見ると、セクションとその見出しの関係を一見うまく表現できているように見えますが、実はHTML5のウリであるはずの「前方互換性」がかなり怪しくなっています。
例えば、
! 見出し1 ああああ !!! 見出し2 いいいい
というレベルを飛ばすような記述は、同じように変換すると
<h1>見出し1</h1> <p>ああああ</p> <h3>見出し2</h3> <p>いいいい</p>
となります。ここで、HTML4とHTML5でスタイルに互換性を持たせるためには、
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <section> <h3>見出し2</h3> <p>いいいい</p> </section> </section> </section>
としなければならないばずなのですが、「暗黙の<section>」は見出しのレベルの差に関係なく1つしか入らないようなので、実際には
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <h3>見出し2</h3> <p>いいいい</p> </section> </section>
と等価になり、さらにこれは
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <h2>見出し2</h2> <p>いいいい</p> </section> </section>
や
<section> <h1>見出し1</h1> <p>ああああ</p> <section> <h1>見出し2</h1> <p>いいいい</p> </section> </section>
などとも意味的には等価でHTML5に準拠したブラウザではこれらを同じように表示することが期待されているようです。
で、これって
ということになり、この理解が正しいなら、CSSでスタイルを記述するのが難しい、かなり筋が悪い仕様だと言わざるを得ません。
もし仮に上のようなHTML4とHTML5の仕様の違いをhikidocなどの生成系の方で吸収しようとすると、
といった実装になるんでしょうが、状態を記憶しておかなければならないようなデザインにせざるを得ないような仕様というのは今時どうなんでしょう?
いっそのこと
とかにしてくれればいいのにとか思いました。
.tright small.(2010/6/25公開)^J2010/6/27: 「追記1」記載、微修正^J2010/6/28: 脚注*5 修正
ソフトバンククリエイティブ社から発行されている「Ruby on Rails 携帯サイト開発技法」の第9章に掲載されているサンプルコード(ファイル4/4)にはセッションIDの発行・管理に不備があるため、セッションハイジャックが可能です。
この本の第9章には「携帯端末特化型セッション管理」と称していわゆる「かんたんログイン」の実装サンプルが掲載されています。第9章の冒頭部分から、その特長に関する記述を抜粋すると、
というものだそうです。最後の項目にいくばくかの配慮を感じられますが、残念ながらこの「一定の防護措置」についての具体的な記述は本文やサンプルコードにはありません。
さておき、このサンプルコードを動かすと、
といった機能を持つサイトができます。
さて、このサイトにPCからアクセスするとそのままではユーザー登録もログインできません(画面はChrome)。^J
.clear. ユーザー登録できないのは、上の画面にもある通りPCからのアクセスでは端末IDが取得できないためです。このあたりの処理については解説が274ページにあり、アクセス元のIPアドレスをチェックして、キャリアに応じたリクエストヘッダから契約者IDを取得するという処理で、一般的な偽装対策やPCからのアクセス対策はなされているように見えます。
ログインでも同様なチェックを経てセッションIDを発行しています(解説はp.267)。が、最終的に生成されるセッションIDは以下のreturn文で返される文字列となっています。
# セッションIDを生成する # ※名前空間が被らないようにプレフィクスを付与しておく return "!"+carrier.to_s+":"+Digest::MD5.hexdigest(mobile_id)
セッションIDの生成に使われている情報は、"!"および":"という2つの文字とアクセスしてきた携帯のキャリア名、そして契約者固有ID(のハッシュ)だけです。契約者固有IDについては、同書p.258に次のような説明があります。
au、Softbankどちらの機種も、ユーザーに確認ダイアログを出すことなく携帯端末の識別情報を送信する仕組みがあったのですが、ドコモ機種だけはそれがなかったのです。 そして2008年3月31日、NTTドコモはついにその問題の解決のために「iモードID」をリリースしました。iモードIDは携帯端末を識別可能なIDであり、クエリパラメータにguid=ONとつけておくと、ユーザーに確認ダイアログを出すことなくリクエストヘッダで送られてきます。
ここで今回の話において重要なのは、NTTドコモの機種であればリンクに「guid=ON」と付けてユーザーを誘導するだけで誰でも簡単にiモードIDを取得することができ、au、Softbankにも同様の仕組みがある、という点です。つまり、
.orangered bold.このサンプルプログラムで使用されているセッションIDはユーザーを識別可能、かつ誰でも入手可能な情報のみを元に生成されているため、他人のセッションIDを容易に推測・再現可能である
と言えます。他人のセッションIDを再現可能であれば、あとはそのセッションIDを偽装してサイトにアクセスすることができればハイジャックが成立します。というわけで、サンプルにおいてセッションIDを偽装したアクセスが可能かどうかを次に考えます。
まず、携帯電話からのアクセスについてですが、フィーチャーフォンとスマートフォンに分け、まずはフィーチャーフォンについて考えます。上にも書いた通り、サンプルではアクセス元のIPアドレスをチェックしてキャリアを判定したのち、そのキャリアに応じたリクエストヘッダ(契約者固有ID)から携帯端末識別情報を取得するという処理になっています。従って、キャリアのゲートウェイを通過してきてなおかつ契約者固有IDを偽装できなければ、セッションハイジャックは成立しません。後述する仕様から、厳密に言うとある特定の条件を満たせば成立する余地はありますが、一般的にはフィーチャーフォンからのアクセスではセッションハイジャックは出来ないといって良いと思います。
次に、スマートフォンとPCからのアクセスについて考えます。どうしてこういうくくりになるかというと、スマートフォンにはキャリアのゲートウェイを利用しない経路からのアクセスが可能であり、その場合はこれ以降の説明に該当するためです。まず、このサンプルは、
(キャリア判別処理や携帯端末識別情報取得に)失敗した場合は、デフォルトのセッションロード処理に制御を戻す
と言う仕様になっています(p. 265)。つまり、キャリアのゲートウェイを介したアクセスに対しては、先ほど引用した推測可能なセッションIDが発行され、PC等からのアクセスに対しては、通常のRailsのセッション管理処理が使われるということになります。さてお立会い、ここでPCブラウザから、携帯電話からのアクセス時に発行されるセッションIDを偽装してアクセスするとどうなるでしょう…
.clear. というわけで、携帯電話からログインしていたセッションのハイジャックができてしまいました。以下、各画像で順に何をやったか解説します。 .flat.
つまり、このサンプルの方法を採用したサイトに関しては、攻撃者視点で書くと、
という手順で、他人に成りすましてサービスを利用することが出来てしまいます。成りすましに必要なすべての情報がサイトに侵入するなどの攻撃的な手段を用いずとも収集可能である、という点が重要です。
契約者固有IDの利用の是非はさておき、今回指摘した脆弱性の根本的な問題点は、
セッションIDとして容易に推測・再現可能な値を利用した
という点にあります。この点だけを解決するもっとも安直な方法は、携帯電話で発行するセッションIDを
return "!"+carrier.to_s+":"+Digest::MD5.hexdigest(mobile_id+"ランダムな秘密の文字列")
とすることです。"ランダムな秘密の文字列"の部分には"ランダムな秘密の文字列"と書くのではなく、サイト管理者が適当にランダムな秘密の文字列を生成して、それを書きます。この"ランダムな秘密の文字列"は第三者に知られてはなりませんし、単純であったり、短かったりしてブルートフォースアタックで簡単に解析できるようなものであってもいけません。また、
return "!"+carrier.to_s+":"+Digest::MD5.hexdigest(mobile_id)+"ランダムな秘密の文字列"
といった具合に、hexdigestの外に出してもいけません。そうしてしまうとクライアントに生の"ランダムな秘密の文字列"が渡され、秘密じゃなくなっちゃいますので。
今回の問題はソースコードが公開されたから簡単に再現できたのであって、ソースや使っているアルゴリズムを非公開にしておけば破られないのではないかと思ったとしたら、それは誤解です。例えば、セッションIDに今回のような値やそのバリエーションが使われているかどうかは、次のようにすれば簡単に推測することができます。
したがって、例えソースやアルゴリズムを非公開にしていても、
誰にでも入手可能で、かつユーザーを識別可能な情報があった場合、それだけから単純に得られる、世に知られているすべてのアルゴリズムのハッシュ値は簡単に推測可能と言える
と思っておいた方がよいでしょう。一方で、独自のハッシュ関数を実装するというのは、理論レベルでの正しい理解が無い限り、きちんとした(この文脈では再現が困難な)ハッシュ関数を作り出すことが難しいという意味で論外です。
全くの余談ですが、本気になった人々が独自のアルゴリズムを使った非公開データ破りにチャレンジするとどういう結果が待っているかというのは、1990年代前後のコンピュータソフトのコピープロテクトとプロテクト解除ソフトのイタチごっこを振り返れば、軍配が上がる先は自明と言えるでしょう。
最後に、「Ruby on Rails 携帯サイト開発技法」第8章でのjpmobileの評価についても触れておきます。
第8章のp.256には
(jpmobileの)trans_sidの実装は、やや古い世代の環境を想定して作られたものであるため、現在においては少々陳腐なものとなってしまっています。 (中略) そこで次の章では、自前で携帯サイトに必要な機能をRuby on Railsに実装する方法について紹介します。 Ruby on Railsでの携帯サイト構築についてより深い理解を得られれば、柔軟かつ堅牢な携帯サイトを思いのままに造れるようになるでしょう。
と書かれていますが、jpmobileの変更履歴によれば、
という修正が3年前に加えられています。今でもドコモの機種は全てCookie利用不可という扱いのようなので(docomo.rbのll.78-80)、「iモード2.0」端末の判定ルーチンの追加は必要ですが、高木浩光さんが2010年06月19日の日記(今こそケータイID問題の解決に向けての下記引用部分)で推奨している、Cookieによる実装に近い形に3年前の時点ですでになっていたことになります。
個人的には、このあたりのメソッドを活用してCookie非対応端末向けの処理を組んだ方が、わざわざ自前ですべての機能を再実装するより、楽してかつ安全な実装ができたんじゃないかという気がしますし、trans_sidというアイデアは確かに古い手法かもしれませんが、その実装はむしろ今もって理にかなっており、陳腐と言う表現は当たらないと思います。
話変わって、セッションIDは推測困難なものにしなければならないというのはかなり昔からある話で、2006年11月1日発行の「安全なウェブサイトの作り方」改訂第2版にも記載があります(追記1:コメントで徳丸浩さんに第1版へのリンクと、そちらにも同様の記述があることを教えていただきました。ありがとうございます。)
厳しいことを言うようですが、本書は安易に自前での機能の実装という選択肢を採ると、思わぬところで落とし穴にはまるパターンの好例になっていると思います。
.green. ====
Before...
- hs [通りすがりさん。 IPアドレスのチェックについては、携帯アプリでは使えない事情があります。 まず、WASForum ..]
- 通りすがり [なるほどですね。 キャリアのGWかどうかを確実に判別するのは難しいとの前提条件があると理解しました。 となると、..]
- hs [通りすがりさん。 本のほうをお持ちで無いと言うことで分かりにくかったかもしれませんが、引用されている箇所は サンプ..]