2.0のcookie session storeを体感する

Rails2.0の変更点で、セッション(session)データの保存先がクッキー(cookie)になったということを、よく目にする。確認してみると、確かに以前はtmp/sessionsフォルダの中に常にセッションファイルがあり、増え続けていたが、2.0環境にしてからはいつも空っぽだ。そうなると、本当にクッキーに保存されているのか?どのように保存されているのか?実際に覗いてみたくなった...。

クッキーを確認する

  • MacOS X版のFirefox2.0のクッキーは、Firefoxの環境設定 >> プライバシー タブ >> Cookieを表示 ボタン、で表示される。
  • 想像以上のクッキーの多さに驚く。一つずつ見ていてはキリが無いので、検索で「localhast」と入力してみる。
  • すると一気に絞り込まれ、Cookie名から「_test_slip202_session」が求めるクッキーだと予想できる。
    • 「_プロジェクト名_session」の書式になっているようだ。
    • これはつまり、config/environment.rbの以下の記述と一致しているのだ。
...(中略)...
Rails::Initializer.run do |config|
...(中略)...
  config.action_controller.session = {
    :session_key => '_test_slip202_session',
    :secret      => '9384c0b735c5d5ff1aafbef9bab1f9b538ef33b0dbade61707d86a0e53ed0a79e649567288ccd877767939d377a512f294a120ac9968ceec49697c6e5e6d2363'
  }
...(中略)...
  • クリックすると、下の方に以下の内容が表示された。
名前 _test_slip202_session
内容 BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%250AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%250Ac2h7AAY6CkB1c2VkewA%253D--1169bd07e96689065f9af999e6b9866351f14c57
ホスト localhost
パス /
送信制限 暗号化の有無によらず常に送信
有効期限 セッション終了時

クッキーを復元する

クッキーを見ることは出来たが、意味の分からない英数字の羅列を見ていても、いまいちピンとこない。自分が理解できる形で見てみたい。調べてみると、内容の文字列の「--」を境にして、
  • 左側がエンコードされたオブジェクトデータ
  • 右側がクッキーの改ざん防止のため、左側のデーターを元に暗号化したチェックデータ
となっているようだ。よって、左側のデータをデコードすれば、理解できる形で、何らかのセッション内容が見えるはずだ!
エンコードの方法
エンコードの方法を正確に確認するため、ソースコードで調べてみた。自分のMacBook OSX10.5環境では以下のファイルが求めるソースコードだった。 環境によっては、以下のパスになっている場合もあるかもしれない。
  • /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/actionpack-1.13.6/lib/action_controller/session/cookie_store.rb
その中から、以下の記述を確認する。
class CGI::Session::CookieStore
...(中略)...
  private
    # Marshal a session hash into safe cookie data. Include an integrity hash.
    def marshal(session)
      data = Base64.encode64(Marshal.dump(session)).chop
      CGI.escape "#{data}--#{generate_digest(data)}"
    end

    # Unmarshal cookie data to a hash and verify its integrity.
    def unmarshal(cookie)
      if cookie
        data, digest = CGI.unescape(cookie).split('--')
        unless digest == generate_digest(data)
          delete
          raise TamperedWithCookie
        end
        Marshal.load(Base64.decode64(data))
      end
    end
...(中略)...
つまり、エンコード・デコードの方法は以下のようにするべきと勝手に解釈した。
  • エンコードは、sessionオブジェクト >> Marshal.dump >> Base64.encode64 >> CGI.escape
  • デコードは、CGI.unescape >> Base64.decode64 >> Marshal.load >> sessionオブジェクト
script/consoleで解読する
test_slip202プロジェクトのディレクトリで、script/consoleを起動して以下のようにやってみた。
>> cookie = "BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%250AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%250Ac2h7AAY6CkB1c2VkewA%253D--1169bd07e96689065f9af999e6b9866351f14c57"

    
=> ["BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%0AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%0Ac2h7AAY6CkB1c2VkewA%3D", "1169bd07e96689065f9af999e6b9866351f14c57"]
ArgumentError: dump format error(0x33) from (irb):109:in `load' from (irb):109
しかし...dump format errorが出て止まってしまう...。ガックリ。 なぜ、dump format errorなのか悩んでいたら、一つ不自然なところに気付いた。CGI.unescapeしているはずなのに、その後のdata文字列の中に"%0A"や"%3D"が見えるのだ。エスケープ文字列を復元しているはずなのに、それはおかしい...。CGI.unescape前の文字列を確認してみると、その箇所は"%250A"と"%253D"となっている。"%25"、つまりアスキーコード表の16進数文字コードで確認すると「%」だ。おそらく、CGI.escapeを2回処理している可能性がある。 ということで、CGI.unescapeも2回処理する手順でやってみた。
>> cookie = "BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%250AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%250Ac2h7AAY6CkB1c2VkewA%253D--1169bd07e96689065f9af999e6b9866351f14c57"

    
=> ["BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%0AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%0Ac2h7AAY6CkB1c2VkewA%3D", "1169bd07e96689065f9af999e6b9866351f14c57"]
=> "BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw\nMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh\nc2h7AAY6CkB1c2VkewA="
=> {"flash"=>{}, :csrf_id=>"ba420274da736844b9bc930ead317033"}
解読できた!(こんな苦労をしなくても、<%= session.inspect %>をビューのどこかで実行すれば、sessionオブジェクトの内容は確認できるのですが...。自分の手でブラウザのクッキーから復元することに魅力を感じたので。)

クッキーの中身を確認

つまり、クッキーの中身は、以下のハッシュオブジェクトということだ!
{"flash"=>{}, :csrf_id=>"ba420274da736844b9bc930ead317033"}
  • "flash"=>{}は、次のアクションまで保持されるセッションデータ。上記は空のハッシュ。つまりflashの中身は空っぽ。
  • :csrf_id=>"ba420274da736844b9bc930ead317033"は、CSRF対策のためのauthenticity_tokenを生成するためのキー。
authenticity_tokenとは?
以前のPUT問題で調べていた時、rails2.0のform_forは、以下のように展開されることを確認した。
<%# ビュー: app/views/slips/edit.rhtml %>
<% form_for(@slip) do |f| %>
<% end %>

<%# 上記は以下のHTMLを生成する。%>
<form action="/slips/11" class="edit_slip" id="edit_slip_11" method="post">
  <div style="margin:0;padding:0">
    <input name="_method" type="hidden" value="put" />
    <input name="authenticity_token" type="hidden" value="bc70df10a269ed11ebcd411fbebf0b732908f40b" />
  </div>
</form>
見えないinputフォームで、:_method=>"put"と同時に、:authenticity_token=>"bc70df10a269ed11ebcd411fbebf0b732908f40b"も送信していたのだ。この:authenticity_tokenを生成する時に、クッキー内の:csrf_idが利用されているようだ。
なぜauthenticity_tokenを送信するのか?
これは、CSRFクロスサイトリクエストフォージェリ)という悪意のあるページへの対策らしい。CSRFについては、検索すれば詳しく書かれたページCSRFについて - willnetさんの日記を見て、なるほど!と思いスッキリ。 :authenticity_tokenによって、外部のページからの不正な送信を無効にしているようだ。(正しい:authenticity_tokenを一緒に送信してきたフォームデータのみ受け付けるようになっている。外部のページは正しい:authenticity_tokenを知ることが出来ないので。)
protect_from_forgeryについて
rails2.0からApplicationControllerは、デフォルトで以下のようにコーディングされている。
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  helper :all # include all helpers, all the time

  # See ActionController::RequestForgeryProtection for details
  # Uncomment the :secret if you're not using the cookie session store
  protect_from_forgery # :secret => '62131e8be3a35fd28465e41d6cd97cf9'
end
protect_from_forgeryという部分が以前から気になっていたが、これによってauthenticity_tokenに不正が無いかをチェックしているようだ。
  • 試しに、Firebugでauthenticity_tokenを改ざんして送信ボタンを押してみると、ActionController::InvalidAuthenticityTokenというエラーが発生する。
  • 次に、送信ボタンを押す前の改ざんページに戻って、protect_from_forgeryをコメントアウトしてから同じように送信ボタンを押してみると、今度は正常に更新されてしまった。
:secretオプションのコメントアウトが気になる...。
protect_from_forgeryの後の:secret => '62131e8be3a35fd28465e41d6cd97cf9'がコメントアウトされていて良いのだろうか?と不安を感じていたが、セッションをクッキーに保存する方式(cookie session store)ならコメントアウトした状態で問題ないようだ。:secretがnilなら、その代わりにクッキー内の:csrf_idが利用される、と自分では理解した。(セキュリティに関することなので、詳細は自分自身で確認することをお勧めします。)
  • /Library/Ruby/Gems/1.8/gems/actionpack-2.0.2/lib/action_controller/request_forgery_protection.rb