at posts/single.html

Ruby の CGI::Session

はてな認証APIで tDiary にログイン (2) で書いた「ログイン前からセッションを発行しているので、Session Fixation の危険性がある」という話に関連して、 CGI::Session の挙動を調べてみた。 やりたいことは、以下の2つ。

  1. セッション Cookie がある時はそれを取得し、無いときはセッションを「作らない」(ログイン状態の判定に使う。ログインしていない人には Cookie を発行しない。)
  2. セッション Cookie の有無に関わらず、新しいセッション Cookie を発行する(ログイン時に使う)

ログイン前から全員にセッション Cookie を発行すればいいじゃんって思うかもしれないけど、そうすると Session Fixation の危険がある。 少なくとも認証した時点で新しいセッション Cookie を発行したほうがいいらしい。 (これは、 Ruby カンファレンスでも前田さん(だったかな?)が少し言及していた)

ri CGI::Session で調べてみたところ、'new_session' というオプションを使えばいいみたい。 以下が ri でのサンプル。

      # We make sure to delete an old session if one exists,
      # not just to free resources, but to prevent the session
      # from being maliciously hijacked later on.
      begin
          session = CGI::Session.new(cgi, 'new_session' => false)
          session.delete
      rescue ArgumentError  # if no old session
      end
      session = CGI::Session.new(cgi, 'new_session' => true)
      session.close

ってことで、以下のように書けばよさそう。

ログイン状態のチェック

ログイン済みの人はセッション Cookie からユーザIDを取得する。 それ以外の人にはセッション Cookie を発行しない。

begin
  session = CGI::Session.new(cgi, 'new_session' => false)
rescue ArgumentError  # if no old session
end
user = session['user_id']

ログイン時のセッション発行

セッション Cookie をすでに発行しているかどうかに関わらず、新しいセッション Cookie を発行する。 (古いセッション Cookie があれば削除する)

begin
  session = CGI::Session.new(cgi, 'new_session' => false)
  session.delete
rescue ArgumentError  # if no old session
end
session = CGI::Session.new(cgi, 'new_session' => true)
session.close
session['user_id'] = user

所感

む…微妙に面倒。 CRUD の原則に従って、以下のようにはできないのかな。

CGI::Session.read # Read: セッションが無ければ nil を返す
CGI::Session.create # Create: セッションの有無に関わらず新しいセッションを発行

現在の CGI::Session.new と同じ動き(あれば取得、無ければ新規作成)は、以下のようにすれば書けるわけだし。

session = CGI::Session.read(cgi)
session = CGI::Session.create(cgi) unless session

一行でこう書いてもいいか。

session = CGI::Session.read(cgi) || CGI::Session.create(cgi)

書いてみた

require 'cgi'
require 'cgi/session'

class CGI
  class Session
    def self.create(request, options = {})
      session = self.read(request, options)
      session.delete if session
      options['new_session'] = true
      CGI::Session.new(request, options)
    end

    def self.read(request, options = {})
      options['new_session'] = false
      begin
        CGI::Session.new(request, options)
      rescue ArgumentError  # if no old session
      end
    end
  end
end

これで準備完了。

関連する日記