今回は新規プロジェクトで実際に遭遇したCSRF保護について学んだことをまとめます。
CSRFとは
CSRF(Cross-Site Request Forgery)を簡単に説明すると、悪意のあるユーザーがサーバーへのリクエストを捏造して正当なものに見せかけ、認証済みユーザーを装うという攻撃手法です。Railsでは、一意のトークンを生成して送信のたびに真正性を確認することでこの種の攻撃から保護します。
(引用サイト:https://techracho.bpsinc.jp/hachi8833/2021_11_26/46891)
これだけでは「なんのこっちゃ、、。」となるため噛み砕いて説明します。
もしかしたら一度は見たことあると思いますが、HTMLのheadタグに「= csrf_meta_tags」なんて見たことはありませんか?
※コードはslim形式になっています。
その「= csrf_meta_tags」が私たちが気づかないうちに、セキュリティーをかけていてくれました。
「= csrf_meta_tags」の役割
この記述の役割としては、サイトのHTMLに一意のトークンを埋め込みます。これと同じトークンはセッションcookieにも保存されます。送信する時にはHTMLに埋められていたCSRFトークンも一緒に送信されます。Railsはページのトークンとセッションcookie内のトークンを比較し、両者が一致することを確認します。
これにより不正なアクセスを防止します。
次に下記のコードはrubyのソースコードです。
ここでは送信されたトークンを検証、トークンの生成などを行っています。
# actionview/lib/action_view/helpers/csrf_helper.rb
def csrf_meta_tags
if protect_against_forgery?
[
tag("meta", name: "csrf-param", content: request_forgery_protection_token),
tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\n").html_safe
end
end
もっと詳しく見ると各メソッドでは下記のような処理となっています。
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
# トークンの値を現在のセッションに設定
def form_authenticity_token(form_options: {})
masked_authenticity_token(session, form_options: form_options)
end
# リクエストごとに異なる真正性トークンのマスキング版を作成する
# マスキングはBREACHなどのSSL攻撃の緩和のため
def masked_authenticity_token(session, form_options: {}) # :doc:
# ...
raw_token = if per_form_csrf_tokens && action && method
# ...
else
real_csrf_token(session)
end
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
end
下記の処理では「セッションからCSRFトークンを取得」「トークンのデコード」を行っています。
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
def real_csrf_token(session) # :doc:
session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
Base64.strict_decode64(session[:_csrf_token])
end
下記では暗号化を行います。
# request_forgery_protection.rb
one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token)
masked_token = one_time_pad + encrypted_csrf_token
Base64.strict_encode64(masked_token)
以上を踏まえて操作の流れを図面に表すと下記のようになります。
画像参照:https://techracho.bpsinc.jp/hachi8833/2021_11_26/46891
ここまでがCSRFトークンの生成と、トークンをHTMLとcookieに付与するまでの流れになります。
トークンの照合
次にユーザーがサイトにフォームを送信する際にトークンの照合の仕組みについて説明をします。
ユーザーがフォームからsubmitすると、フォームの他のデータとともにCSRFトークンが送信されます。
その際にどフォルトで設定されている「before_action」が機能します。
before_action :verify_authenticity_token, options
# actionpack/lib/action_controller/metal/request_forgery_protection.rb
def verify_authenticity_token # :doc:
# ...
if !verified_request?
# エラー処理 ...
end
end
# ...
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
# request_forgery_protection.rb
def any_authenticity_token_valid? # :doc:
request_authenticity_tokens.any? do |token|
valid_authenticity_token?(session, token)
end
end
# request_forgery_protection.rb
def unmask_token(masked_token) # :doc:
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
コードデータ参照:https://techracho.bpsinc.jp/hachi8833/2021_11_26/46891
※照合が成功する場合のフローに注目できるよう、一部のコードを省略しています。
「verify_authenticity_token」はすごく簡単にまとめると、「復号し、トークンを比較」という処理を行っています。
最後に下記の処理でリクエストが承認されます。
# request_forgery_protection.rb
def compare_with_real_token(token, session) # :doc:
ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end
まとめ
今回はコードの詳細などは細かく行いませんでしたが、要するにCSRF保護を設定することで、外部からの攻撃を防ぐことができます。