Rails 2.0・その12(CSRFを勝手に防止)
CSRF とは簡単に言うと、ある特定のURLがDBに挿入したり更新したりすると仮定します。そして、そのURLにアクセスしまくってDBの値を変えまくることです(だと思う・・・)。
script/generate scaffold した時点で、もう既に対応済みになっていて何もすることはありませんでした。
じゃあ、どこで設定されているか、というと、app/controllers/application.rb をご覧ください。
protect_from_forgery # :secret => '6d0b20bbf9203508337aff3214f79efb7a0'
protect_from_forgery と書くだけです。終わり。
そうすると、POST・DELETE・PUTの時(つまりGETじゃあないとき)に、Rails が、authenticity_tokenという hidden パラメータのようなものを付け加えてくれてこれでチェックされます。
Rails がチェックした時に、不正なアクセスだったら、ActionController::InvalidAuthenticityTokenという例外が発生します。
とここまでが概要です。いかがでしたでしょうか。以下の行からは、もうちょっと掘り下げて書いてみようかなと思います。といっても期待はしないでねと。
さてさて、上記の protect_from_forgery の箇所で、:secret から後ろがコメントアウトされていました。
クッキーセッション(Rails2.0.2ではデフォルトざます)を使っている時は、このコメントを外さないでください。
DBセッションやメモリセッションを使っている時は、このコメントを外してください。というか、:secret の値を書き換えて、さらにその値が外部のスパイにばれないようにしてください。
じゃあなんでコメントアウト外したり外さなかったりなの?とか、そもそもCSRF防止になるの?とかは、以下に更に掘り下げて書いてみたいと思いますが、超長くなるので、はっきり言って飽きるかもです。
1.デフォルトであるクッキーセッションを使っている場合
CSRF されているかどうかは、
クライアントの session[:csrf_id] から生成されたチェックサム
と
クライアントから送られてきた authenticity_token
が同じかどうかで判断します。
actionpack-2.0.2/lib/action_controller/request_forgery_protection.rb
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end
・
・
・
def form_authenticity_token
@form_authenticity_token ||= if request_forgery_protection_options[:secret]
authenticity_token_from_session_id
elsif session.respond_to?(:dbman) && session.dbman.respond_to?(:generate_digest)
authenticity_token_from_cookie_session
elsif session.nil?
raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session."
else
raise InvalidAuthenticityToken, "No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)."
end
end
・
・
・
def authenticity_token_from_cookie_session
session[:csrf_id] ||= CGI::Session.generate_unique_id
session.dbman.generate_digest(session[:csrf_id])
end
ちなみに、authenticity_token を作るときには、上記の form_authenticity_token を呼びます。
つまり、自前で authenticity_token=<%= form_authenticity_token %> 的なことができます。
なので、結局の所、クライアントの session[:csrf_id] から param[:authenticity_token] と同じ値を作ることができるかどうかを見ていると言っていいでしょう。
じゃあ、改ざんは?できるんじゃあないの?と思いますが、まず、session[:csrf_id] を改ざんしたいといった場合は、セッションのチェックサムも対応する値に変えなくてはいけません。
どういうことかというと、クッキーセッションの場合はブラウザのクッキーに
セッションの値--セッションの値から生成したチェックサム
という長ーい文字列が設定されています。
session/cookie_store.rb
def close
if defined?(@data) && [email protected]?
updated = marshal(@data)
raise CookieOverflow if updated.size > MAX
write_cookie('value' => updated) unless updated == @original
end
end
・
・
・
def generate_digest(data)
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
end
・
・
・
def marshal(session)
data = Base64.encode64(Marshal.dump(session)).chop
CGI.escape "#{data}--#{generate_digest(data)}"
end
この「セッションの値」を変える(session[:csrf_id]の値をいじくる)ということは、「セッションの値から生成したチェックサム」も併せて変えなければ Rails が有効なクッキーとして扱ってくれません。
session/cookie_store.rb
def restore
@original = read_cookie
@data = unmarshal(@original) || {}
end
・
・
・
def unmarshal(cookie)
if cookie
data, digest = CGI.unescape(cookie).split('--')
unless digest == generate_digest(data)
delete
raise TamperedWithCookie
end
Marshal.load(Base64.decode64(data))
end
end
でもこのチェックサム、サーバ側に書いてある文字列がないと生成できないようになっています。
なので、勝手にセッションの値は書き換えられません。
サーバ側に書いてある文字列っていうのが以下 :secret の値です。
config/environment.rb
config.action_controller.session = {
:session_key => '_scaf2_session',
:secret => 'c0b6ae4957515a57bb58d880150ecdd71faa8990ff0d7ff35635cd55'
}
じゃあ、authenticity_token は改ざんできるか、ということですが、これは、もともと session[:csrf_id] から作られたものですから、session[:csrf_id] の値を何に変えたらいいのか分かりませんし、「セッションの値から生成したチェックサム」の値も変えなくてはいけませんから、それはできない、ということで無理なのでした。
ここまで、すごく長いなあ。誰もこのエントリ読まなさそうだなあorz
2.デフォルトであるクッキーセッションを使っていない場合
CSRF されているかどうかは、
クライアントのセッションIDとprotect_from_forgeryのオプション値:secretとのチェックサム
と
クライアントから送られてきた authenticity_token
が同じかどうかで判断します。
actionpack-2.0.2/lib/action_controller/request_forgery_protection.rb
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end
・
・
・
def form_authenticity_token
@form_authenticity_token ||= if request_forgery_protection_options[:secret]
authenticity_token_from_session_id
elsif session.respond_to?(:dbman) && session.dbman.respond_to?(:generate_digest)
authenticity_token_from_cookie_session
elsif session.nil?
raise InvalidAuthenticityToken, "Request Forgery Protection requires a valid session. Use #allow_forgery_protection to disable it, or use a valid session."
else
raise InvalidAuthenticityToken, "No :secret given to the #protect_from_forgery call. Set that or use a session store capable of generating its own keys (Cookie Session Store)."
end
end
・
・
・
def authenticity_token_from_session_id
key = if request_forgery_protection_options[:secret].respond_to?(:call)
request_forgery_protection_options[:secret].call(@session)
else
request_forgery_protection_options[:secret]
end
digest = request_forgery_protection_options[:digest] ||= 'SHA1'
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(digest), key.to_s, session.session_id.to_s)
end
基本的に、クライアントのクッキーにセッションIDが設定されていて、サーバ側にはセッションIDに紐付いたsession情報がファイルやDBやメモリに格納されています。
チェックサム(authenticity_token)は、protect_from_forgery のオプションに指定されている :secret の値と、ブラウザから渡されたセッションIDから上記の authenticity_token_from_session_id のように生成されます。
以下は、protect_from_forgery のオプションに:secretを指定している、の所です。
app/controllers/application.rb
protect_from_forgery :secret => '6d0b20bbf9203508337aff3214f79efb7a0'
セッションIDが同じであれば、authenticity_token もずーっと一緒なのですね。
この場合の改ざんの心配なのですが、セッションIDを改ざんするとauthenticity_tokenも変えなきゃならないですが、:secretの値が分からないので無理ですね。
authenticity_tokenの値を改ざんすると、セッションIDも変えなきゃならなくて、:secretの値が分からないので無理ですね。
ということでした。いやぁ、書くの大変だったけどこの情報が役に立ったという人が世に1人出ればOKにしておきます。
【広告】
COMMENT
コメントが嬉しかったのでもうそろそろ記事の更新を久しぶりにしてみよーかなー?
謎のエラーで困ってました.
役に立ちました!!
ありがとうございます.
ありがとう!!
rubyは個人的にはソースコードが読みやすいので好きです!
すごく勉強になりました!
ありがとうございます。
この記事、勢いで書ききった記事です。
記事を書いた次の日に、この記事を自分で読み返してみて、ふむふむ、すごい、なるほど、と早速自分で書いた内容を忘れていて思い出すことになる始末。
でもどうやら世の中の1人以上の方の役に立っているようなのでなんだかマンモスうれピーです(っていう語彙は今使うとブラックな表現になっちゃうのかな?)
始めるにあたり、この言語ではセキュリティ対策ってどうやるんだろう、CSRFを防ぐにはどうすればいいんだろう、と思っていたのですが、こんなにあっさりしているんですね。
ますます使ってみようと思いました。執筆おつかれさまでした。
もしかしてそのうち ror.start と一行書くだけでWebサイトが動作してしまうぐらい進化してしまうかもしれませんんね。