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環境では以下のファイルが求めるソースコードだった。- /Library/Ruby/Gems/1.8/gems/actionpack-2.0.2/lib/action_controller/session/cookie_store.rb
- /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 ...(中略)...つまり、エンコード・デコードの方法は以下のようにするべきと勝手に解釈した。
script/consoleで解読する
test_slip202プロジェクトのディレクトリで、script/consoleを起動して以下のようにやってみた。>> cookie = "BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%250AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%250Ac2h7AAY6CkB1c2VkewA%253D--1169bd07e96689065f9af999e6b9866351f14c57"=> ["BAh7BzoMY3NyZl9pZCIlYmE0MjAyNzRkYTczNjg0NGI5YmM5MzBlYWQzMTcw%0AMzMiCmZsYXNoSUM6J0FjdGlvbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhh%0Ac2h7AAY6CkB1c2VkewA%3D", "1169bd07e96689065f9af999e6b9866351f14c57"]しかし...dump format errorが出て止まってしまう...。ガックリ。 なぜ、dump format errorなのか悩んでいたら、一つ不自然なところに気付いた。CGI.unescapeしているはずなのに、その後のdata文字列の中に"%0A"や"%3D"が見えるのだ。エスケープ文字列を復元しているはずなのに、それはおかしい...。CGI.unescape前の文字列を確認してみると、その箇所は"%250A"と"%253D"となっている。"%25"、つまりアスキーコード表の16進数文字コードで確認すると「%」だ。おそらく、CGI.escapeを2回処理している可能性がある。 ということで、CGI.unescapeも2回処理する手順でやってみた。ArgumentError: dump format error(0x33) from (irb):109:in `load' from (irb):109>> 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' endprotect_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