session data integrity

environment.rb が変わっています。こんな感じで、config.action_controller.session = {:session_key => .. , :secret => .. } をつけないと怒られます。

  characters = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
  secret = Array.new(48){characters[rand(characters.size)]}.join

  config.action_controller.session = {
    :session_key => "_webjourney_session",
    :secret => secret
  }


なんで?ともって、Rails 2.0 の セッション周りのコードを読む。
Rails 2.0では、セッションのデフォルトストアがCookieになりました、という噂からactionpack/lib/action_controller/session/cookie_store.rb を参照します。

  # 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

こんな感じで、Cookieに対してセッションデータを読み書きします。marshalして渡しておいて、unmarshalで元に戻す、と。

    # Marshal a session hash into safe cookie data. Include an integrity hash.
    def marshal(session)
      data = ActiveSupport::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(ActiveSupport::Base64.decode64(data))
      end
    end

つまり、Cookie に書き出すときに、(本物のデータ)--(digestデータ)で書き出すルールにしておいて、Cookieを受け取ったときは、digestデータが一致することで信用するようです。さらに、generate_digestメソッド。

  # 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

ここで、秘密鍵 @secret がでてきているわけですが、これをenvorinment.rbで指定しろ、ということのようです。digestが異なると例外が発生するようなので、アプリケーションで固定にした方がよさそうですね。最初にあげた例では、起動時に乱数でとってきてたので、これではまずい(ユーザーがアクセス中に再起動すると例外がかならず発生する)。


というわけで、

# ruby -e "c=('a'..'z').to_a+('A'..'Z').to_a+('0'..'9').to_a;puts Array.new(48){c[rand(c.size)]}.join"

で48文字生成しておいて、

  config.action_controller.session = {
    :session_key => "_webjourney_session",
    :secret => "2XTUIOx7hbFeCYnxxKMyMpEKfnHgq78PlGhzYS9hsyD0ByCS"
  }

で書くと。

それにしても、これで、セッションはサーバー側で保持するから安全、という眉唾ものの話が減りそうで一安心。たまに、

session[:current_password] = "hogefuga"

とかいうコードを見かけるものだから。

ちなみにWebJourneyではsessionを使っている箇所は1カ所、認証済ユーザーIDを保持するため、でした。JavaScriptでもっていてもいいんだけれど、レベルの話です。