雑記

2000|01|
2003|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|
2007|01|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|09|11|
2009|02|03|05|06|07|08|10|11|12|
2010|01|03|04|05|06|07|08|09|10|
2011|05|06|09|10|
2012|03|07|09|12|
2013|01|02|04|05|07|08|10|11|
2014|04|05|08|10|12|
2015|01|05|
2016|09|

2010-06-01 [長年日記]

HTML5のセクションと見出し

出張に向かう電車の中で「HTML5 & API入門」を読んでいたんですが、第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に準拠したブラウザではこれらを同じように表示することが期待されているようです。

4.4.11 Headings and sections

で、これって

  • あるタグ(h1)が、他のタグ(section)の影響を受けてスタイルが変わる
  • 逆に違うタグ(上の例ではh4とh1)を記述しても、sectionの段数が同じならそれらの違いは無視されて同じスタイルが適用される
  • 暗黙のセクションが1段だけ入るというルールが、さらに事態をややこしくしている

ということになり、この理解が正しいなら、CSSでスタイルを記述するのが難しい、かなり筋が悪い仕様だと言わざるを得ません。

もし仮に上のようなHTML4とHTML5の仕様の違いをhikidocなどの生成系の方で吸収しようとすると、

  • 最後に出てきた見出しのレベルを記憶しておく
  • 見出しを読み込んだら最後の見出しとのレベルの差を計算してsectionタグを入れ子にする

といった実装になるんでしょうが、状態を記憶しておかなければならないようなデザインにせざるを得ないような仕様というのは今時どうなんでしょう?

いっそのこと

  • h1〜h6は全て廃止(あるいはHTML4との互換性が完全に保てる形で残す)
  • セクションの入れ子関係は<section>タグで表現する
  • セクションのタイトルは<title>(<t>)タグ、サブタイトルは<subtitle>(<subt>)タグを使う

とかにしてくれればいいのにとか思いました。


2010-06-23 [長年日記]

[Rails] 「Ruby on Rails 携帯サイト開発技法」第9章のサンプルコードに含まれる脆弱性について

.tright small.(2010/6/25公開)^J2010/6/27: 「追記1」記載、微修正^J2010/6/28: 脚注*5 修正

まとめ

ソフトバンククリエイティブ社から発行されている「Ruby on Rails 携帯サイト開発技法」の第9章に掲載されているサンプルコード(ファイル4/4)にはセッションIDの発行・管理に不備があるため、セッションハイジャックが可能です。

解説

この本の第9章には「携帯端末特化型セッション管理」と称していわゆる「かんたんログイン」の実装サンプルが掲載されています。第9章の冒頭部分から、その特長に関する記述を抜粋すると、

  • Cookieの代わりにリクエストヘッダからの情報を使ってセッション管理を行うという、携帯サイトならではのセッションの実装
  • セッションIDを端末識別情報から生成
  • 端末に対するセッションIDは常に固定され、サーバー側がセッション情報を保持してさえいれば、ログイン状態は常に保たれたまま
  • ユーザーからすればログインと言うわずらわしい操作が不要になる
  • 利便性は大きく向上するのですが、これではセキュリティ性をあまりにも下げすぎてしまうので、重要な操作にはパスワードの入力を求めるなどの一定の防護措置を講じる必要はあります

というものだそうです。最後の項目にいくばくかの配慮を感じられますが、残念ながらこの「一定の防護措置」についての具体的な記述は本文やサンプルコードにはありません。

さておき、このサンプルコードを動かすと、

  • ユーザー登録(画面はSoftbank携帯電話のスクリーンショット)^J
  • .clear. いわゆるかんたんログイン^J
  • .clear. プロフィール編集^J
  • .clear. ログアウト(トップページに戻る)

といった機能を持つサイトができます。

さて、このサイトに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.

下準備
1枚目
携帯電話でサンプルサイトにアクセスし、ユーザー登録をすませてログインします。
2枚目
3枚目
Cookieを設定した後にもう一度トップページにアクセスし、「ログイン」リンクをクリックすると、ログインに成功して先ほど携帯から登録した名前が表示されています。
4枚目
プロフィールも携帯から設定したものであることが確認できます。
5枚目
プロフィールを編集してみます。
6枚目
携帯電話でプロフィール画面に飛ぶと、プロフィールが改ざんされていることが確認できました。

つまり、このサンプルの方法を採用したサイトに関しては、攻撃者視点で書くと、

  1. あらかじめ他のサイト等をつかって、被害者の契約者固有IDを調べておく
  2. 攻撃対象となるサイトにアクセスして、セッションIDが保存されているCookieの名前を調べる
  3. 偽装したセッションIDを使ってアクセスする

という手順で、他人に成りすましてサービスを利用することが出来てしまいます。成りすましに必要なすべての情報がサイトに侵入するなどの攻撃的な手段を用いずとも収集可能である、という点が重要です。

セッションIDを推測困難にする

契約者固有IDの利用の是非はさておき、今回指摘した脆弱性の根本的な問題点は、

セッションIDとして容易に推測・再現可能な値を利用した

という点にあります。この点だけを解決するもっとも安直な方法は、携帯電話で発行するセッションIDを

return "!"+carrier.to_s+":"+Digest::MD5.hexdigest(mobile_id+"ランダムな秘密の文字列")

とすることです。"ランダムな秘密の文字列"の部分には"ランダムな秘密の文字列"と書くのではなく、サイト管理者が適当にランダムな秘密の文字列を生成して、それを書きます。この"ランダムな秘密の文字列"は第三者に知られてはなりませんし、単純であったり、短かったりしてブルートフォースアタックで簡単に解析できるようなものであってもいけません。また、

return "!"+carrier.to_s+":"+Digest::MD5.hexdigest(mobile_id)+"ランダムな秘密の文字列"

といった具合に、hexdigestの外に出してもいけません。そうしてしまうとクライアントに生の"ランダムな秘密の文字列"が渡され、秘密じゃなくなっちゃいますので。

ソースが非公開だったら…

今回の問題はソースコードが公開されたから簡単に再現できたのであって、ソースや使っているアルゴリズムを非公開にしておけば破られないのではないかと思ったとしたら、それは誤解です。例えば、セッションIDに今回のような値やそのバリエーションが使われているかどうかは、次のようにすれば簡単に推測することができます。

  1. 携帯電話を用意して、契約者固有IDを調べる
  2. 以下のコマンドを実行して、契約者固有IDのハッシュ値を求める(コマンドを変えれば、アルゴリズムがSHA-1だろうがXORだろうがBase64だろうが同様にハッシュ値を得ることは可能)
  3. ターゲットサイトに1.で用意した携帯電話を使ってユーザー登録し、セッションIDを抜き出す。
  4. セッションIDに、2.で得られた文字列が含まれるかどうかを調べる

したがって、例えソースやアルゴリズムを非公開にしていても、

誰にでも入手可能で、かつユーザーを識別可能な情報があった場合、それだけから単純に得られる、世に知られているすべてのアルゴリズムのハッシュ値は簡単に推測可能と言える

と思っておいた方がよいでしょう。一方で、独自のハッシュ関数を実装するというのは、理論レベルでの正しい理解が無い限り、きちんとした(この文脈では再現が困難な)ハッシュ関数を作り出すことが難しいという意味で論外です。

全くの余談ですが、本気になった人々が独自のアルゴリズムを使った非公開データ破りにチャレンジするとどういう結果が待っているかというのは、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. ====

本日のツッコミ(全9件) [ツッコミを入れる]

Before...

- hs [通りすがりさん。 IPアドレスのチェックについては、携帯アプリでは使えない事情があります。 まず、WASForum ..]

- 通りすがり [なるほどですね。 キャリアのGWかどうかを確実に判別するのは難しいとの前提条件があると理解しました。 となると、..]

- hs [通りすがりさん。 本のほうをお持ちで無いと言うことで分かりにくかったかもしれませんが、引用されている箇所は サンプ..]