Ruby CGI::Session(CGI::Cookie)をSameSite属性対応する
ruby標準ライブラリ CGI でセッション(やCookie)を利用していて、なおかつCookieにSameSite属性を付けたい人向けの記事です。
Railsなんかでは対応されているようですが、標準の CGI::Cookie や CGI::Session クラスではSameSite属性に対応していないんですね。自力でなんとかしてみます
過去の対応方法(ruby-2.7.7では無効)
今までは単純に path
などの末尾に ; SameSite=xxx
付けてしまえば大丈夫でした。
sample.cgi
#!ruby
#-*- coding:utf-8 -*-
require 'cgi'
require 'cgi/session'
cgi = CGI.new
path = ENV['SCRIPT_NAME'] ? File.dirname(ENV['SCRIPT_NAME']) : '/'
path += '; SameSite=Strict'
session = CGI::Session.new(cgi, {"session_path" => path})
session['counter'] ||= 0
session['counter'] = session['counter'].to_i + 1
cgi.out("type" => "text/plain") do
"counter: #{session['counter']}"
end
ただしこの方法は ruby 2.7.7 で CGI::Cookie に脆弱性の対応が入り使えなくなりました。
path
は正規表現でチェックされるようになり、;
などが含まれているとエラーです。
ruby-2.7.7以降の対応方法
CGI::Cookie, CGI::Sessionクラスを改造することにしました
cgisamesite.rb
#-*- coding:utf-8 -*-
require 'cgi'
require 'cgi/session'
class CGI::Cookie
attr_accessor :samesite
SAMESITE_VALUE_RE = %r"\A(Lax|Strict|None)\z"i
alias :initialize_original :initialize
alias :to_s_original :to_s
def initialize(name = "", *value)
@samesite = nil
initialize_original(name, *value)
if !name.kind_of?(String) && name.has_key?('samesite')
self.samesite = name['samesite']
end
end
def samesite=(str)
if str and !SAMESITE_VALUE_RE.match?(str)
raise ArgumentError, "invalid samesite: #{str.dump}"
end
@samesite = str
end
def to_s
buf = to_s_original
buf << "; SameSite=#{@samesite}" if @samesite
buf
end
end
class CGI::Session
alias :initialize_original :initialize
def initialize(request, option={})
initialize_original(request, option)
session_key = option['session_key'] || '_session_id'
session_id = @session_id
request.instance_eval do
@output_cookies = [
::CGI::Cookie::new(
"name" => session_key,
"value" => session_id,
"expires" => option['session_expires'],
"domain" => option['session_domain'],
"secure" => option['session_secure'],
"path" => option['session_path'] || (ENV["SCRIPT_NAME"] ? File::dirname(ENV["SCRIPT_NAME"]) : ""),
"samesite" => option['session_samesite']
)
] unless option['no_cookies']
end
end
end
これを require して以下のように使います
sample2.cgi
#!ruby
#-*- coding:utf-8 -*-
require 'cgi'
require 'cgi/session'
require './cgisamesite.rb'
cgi = CGI.new
session = CGI::Session.new(cgi, {"session_samesite" => "Strict"})
session['counter'] ||= 0
session['counter'] = session['counter'].to_i + 1
cgi.out("type" => "text/plain") do
"counter: #{session['counter']}"
end
CGI::Session.new の第2引数に session_samesite
オプションを渡すだけでよくなりました。
CGI::Cookie や CGI::Session の構造が変わらない限りは使えると思います