Rails 2.0のセッション話

Rails 2.0はセッションはCookieに入れる、というのを読んで*1Cookie-sessionなんじゃらほい、と思ったのでちょっとソースを見てみました。

「ふつう」セッションに入れるようなちょっとしたデータは4K制限のあるCookieでも十分のはずだよねぇ、ということでセッションにいれる情報をMarshalしてCookieに入れちゃいましょう、というのがこの方式のポイントです。
で、Cookieに入れるっていうとユーザが自由自在にいじれるわけで、信用していいんだっけ?というのが気になったわけです。

見てみた結果はまぁ大丈夫そう。データに突っ込んだ内容とそのdigestの両方をCookieに入れて、受け付けたときはそれを検証するという手順になってるみたいです。digestを生成するときはsecretも必要になりますが、それがconfig/environment.rbで指定することになったアレですね。どこで使ってるのかと思ったらこんなところに。

参考: actionpack-2.0.1/lib/action_controller/session/cookie_store.rb

参考としてソース読んだときの要点を抜き出したつもりなんですが、なんかソースをそのまま乗っけたのと変わんない気がしてきました orz。

いちおう restore => unmarshal => generate_digest => initialize と close => marshal => generate_digest という順番に読むと雰囲気が伝わるんじゃないかと思います。

# actionpack-2.0.1/lib/action_controller/session/cookie_store.rb
class CGI::Session::CookieStore
  def initialize(session, options = {})
    ...
    # 59行目付近
    # Keep the session and its secret on hand so we can read and write cookies.
    @session, @secret = session, options['secret']

    # Message digest defaults to SHA1.
    @digest = options['digest'] || 'SHA1'
    ...
  end
  ....

  # 96行目付近
  # Restore session data from the cookie.
  def restore
    @original = read_cookie
    @data = unmarshal(@original) || {}
  end

  ....
  # Write the session data cookie if it was loaded and has changed.
  def close
    if defined?(@data) && !@data.blank?
      updated = marshal(@data)
      raise CookieOverflow if updated.size > MAX
      write_cookie('value' => updated) unless updated == @original
    end
  end

  ....
  # 122行目付近
  # Generate the HMAC keyed message digest. Uses SHA1 by default.
  def generate_digest(data)
    key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
    OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
  end

  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
end