Rails6 のセッションの扱いと、クッキーからの復元方法

2020-09-01

実のところ、ウェブサービスのセッションつーものがどうやって実現されているのかをあまり理解してなかった。 特にRuby on Railsではセッションの内容自体をサーバのDBじゃなくてクッキーに保存するということらしいが、その場合のセキュリティ的なこともあまり自信がなかった。 のでちょっと調べてみた。

先にまとめ

  • Railsではセッションの内容がクッキーでやりとりされる(デフォルトの場合)
  • Rails6 ではクッキーが暗号化されているので、クライアント側で改ざんはできない
  • クッキーには httpOnly が指定されているので、サーバに送られるだけでJavaScriptから読み出されることはない

Railsでのセッションの使用方法

例えばユーザにログインさせる場合に、なんらかの認証をした後にコントローラでセッションにユーザIDをセット:

class SessionsController < ApplicationController
def login
# user = ...
reset_session
session[:user_id] = user.id # これ
end
end

すると以降コントローラで session[:user_id]nil じゃなければログイン済みとして扱う、などとする。

ここで疑問だったのは、

  • クライアントでクッキーを書き換えられたら他人になりすましてログインされてしまうのでは?
  • クッキーを盗まれたらまずいんじゃ?

というあたりが不思議だった。

セッションクッキーの内容とクライアント側での書き換え

Railsではセッションクッキーには値を生のままではなく暗号化して送るようになっているので、秘密鍵を知らない限り復元も再暗号化もできないはず。

なのでクライアント側で内容を書き換えるということはできないので、 直接 user_id を書き換えることで他人としてログインできてしまう、ということはない。

ただし内容自体をクッキーでやり取りしているので、クライアント側でのクッキーの削除による情報消失や、 クッキー内容をコピーしておいて後で上書きして過去の状態に巻き戻るというのはあり得る (CookieStoreセッションに対する再生攻撃

クッキーの盗み見・漏洩

まずクッキーが盗まれたらまずいんじゃ?というのはまったくその通り。 とはいえクッキーストアじゃなくてセッションIDだけを送る方式でも、盗まれたらセッションが奪われてしまってまずいのは同じ。 なので盗まれないようにすることが重要。

https 経由であれば、通信経路で内容を盗み見られても内容はわからないはず。

ウェブサイトでJavaScriptを使用している場合にうかつなモジュールを組み込んだことで、 またはXSSによって埋め込まれたコードでクッキーを盗み取られるようなことはないのか。

Railsのセッションクッキーは httpOnly となっていてJavaScriptからアクセスできないので、スクリプト実行による漏洩はおきない。

セッションクッキーの内容の確認

Google Chromeではメニューから検証>Application>Cookies 内でクッキーが確認できる。

_myapp_session という名前が使われている。

セッションクッキーから内容を復元

クライアント側では改ざん不能といってもサーバ側では扱える必要があるので、復元する方法があるはず。

Decrypt Rails 6.0 beta session cookies

# `bundle exec ruby ...` で直接実行したい場合、以下を実行させる
#require File.expand_path('../../../config/environment', __FILE__)

def verify_and_decrypt_session_cookie(cookie, secret_key_base = Rails.application.secret_key_base)
config = Rails.application.config
cookie = CGI::unescape(cookie)
salt = config.action_dispatch.authenticated_encrypted_cookie_salt
encrypted_cookie_cipher = config.action_dispatch.encrypted_cookie_cipher || 'aes-256-gcm'
# serializer = ActiveSupport::MessageEncryptor::NullSerializer # use this line if you don't know your serializer
serializer = ActionDispatch::Cookies::JsonSerializer

key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
secret = key_generator.generate_key(salt, key_len)
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: serializer)

session_key = config.session_options[:key].freeze
encryptor.decrypt_and_verify(cookie, purpose: "cookie.#{session_key}")
end

if $0 == __FILE__
ARGV.each do |arg|
puts verify_and_decrypt_session_cookie(arg)
end
end

上記スクリプトをRailsアプリディレクトリ内に保存し、 rails runner でコマンドライン引数に与えてやると復元できる:

$ session_cookie='wWL47h2B...--TImX...--e8t%...'  # ブラウザで確認したセッションクッキーの内容
$ rails runner verify_and_decrypt_session_cookie60beta1.rb $session_cookie
{"session_id"=>"2b569463...", "user_id"=>"foobar", "_csrf_token"=>"fp6YvDbg..."}
  • salt"authenticated encrypted cookie" (デフォルト設定のままでいいんだろうか?)
  • encrypted_cookie_ciphernil (なので 'aes-256-gcm' が使われる)
    • AES: Advanced Encryption Standard
    • GCM: Galois/Counter Mode
  • secret_key_base は128文字の16進数文字列(=512ビット)
    • rails new 時に config/master.keycredentials.yml.enc が自動生成され、その中で保持されている
    • development 時には tmp/development_secret.txt が使われる
  • key_len32
  • session_key"_myapp_session"

セッションIDの取得

サーバ側でセッションIDを取得するには、いろいろ方法がある:

session.id
session[:session_id]
request.session_options[:id]

セッションクッキーの削除

ユーザがログアウトした場合に、ブラウザ側でセッションクッキーが削除されるようにする必要がある。 Railsでは session.clear ですべての項目、 session.delete で個別に削除できる。

ただし項目は削除されるが勝手に再度作成されるのか、セッションクッキー自体は送られていて、ブラウザから _myapp_session 自体が削除されるわけではない (session にアクセスしない状態や session.clear 後でも勝手にセッションクッキーが送られている)。 セッションクッキーを送らない、というようなことはできないぽい…。

またセッションを削除してもサーバ側で管理しているわけではないので、クライアント側で過去の内容を保持しておいて復元した場合には無効とならずに復帰されてしまう。

そもそもセッションとは?

ここまできて、実のところ「セッション」という単語が指す内容の想定がずれているんじゃないかという気がしてきた…。 よくショッピングカートの例とか出てくるけど、例えばアマゾンのショッピングカートとかを想像すると、 ブラウザを閉じても別端末でアクセスしてもカートに追加した商品が保持されるが、こういうものはセッション情報には含まないのではないかという気がしてきた。

もっと一時的な内容、例えば複数画面に渡るユーザ情報登録やアンケートなど、ブラウザのページを閉じたら捨てていいような内容のことを指すのが適切な気がする。

参考