RailsのCSRF保護機能

目次

こんにちは、キラメックスでテックアカデミーの開発をしている久保田です。今回はCSRFのおさらいとRailsCSRF保護機能がどのように実現されているのかコードリーディングしつつ整理してみました。

CSRFとは?

安全なウェブサイトの作り方 クロスサイト・リクエスト・フォージェリ
安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ)
安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ):IPA 独立行政法人 情報処理推進機構

具体的な被害

  • アカウントの削除
  • パスワード変更によるアカウント乗っ取り
  • データの更新・削除

実際に起きた事件・騒動

  • 横浜市の小学校襲撃予告事件

    不正プログラムを仕込んだウェブサイトのリンク先をインターネット掲示板に貼り付け、クリックした都内の男性(20)のパソコンから自動送信で小学校の襲撃予告を書き込んだ

横浜市の小学校襲撃予告容疑、片山被告を追送検: 日本経済新聞

大量の「はまちちゃん」を生み出したCSRFの脆弱性とは? - ITmedia エンタープライズ

対策概要(ワンタイムトークン方式)

ワンタイムトークンを用いた対策では、4つのステップで構成されています。

  1. レンダリング時に一意のトークンをHTMLに埋め込む
  2. これと同じトークンをセッションに保存
  3. ユーザーのPOSTリクエスト時に、埋められていたトークンも一緒に送信
  4. 送信されたトークンとセッションのトークンを比較し、一致するかを確認

RailsCSRF保護機能

Railsではviewとcontrollerに書かれた2つのメソッドによりCSRF対策を行うことができます。

csrf_meta_tags

上述のステップ1と2にあたります。

# app/view/layouts/application.html.erb

<%= csrf_meta_tags %>

このタグは以下のHTMLを生成します。

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg==" />

"csrf-token"タグのcontent属性にある長い文字列がワンタイムトークンにあたります。csrf_meta_tagsの中身をみてトークンが埋め込まれるまでにどんな処理がされているかを追っていきたいと思います。コードは説明のために適宜省略しています。ご了承ください。

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

form_authenticity_tokenが処理の始まりです。このメソッドはmasked_authenticity_tokenにセッションを渡しています。

def form_authenticity_token(form_options: {})
  masked_authenticity_token(session, form_options: form_options)
end

sessionを渡されたmasked_authenticity_tokenの中身をみてみましょう。

def masked_authenticity_token(session, form_options: {})
  raw_token = real_csrf_token(session)

  one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH) ③          # 生トークンの暗号化で使うワンタイムパッドを生成
  encrypted_csrf_token = xor_byte_strings(one_time_pad, raw_token) ④             # 平文トークンをビットごとのXOR操作で暗号化
  masked_token = one_time_pad + encrypted_csrf_token ⑤                           # ワンタイムパッド文字列を暗号化文字列の前に追加
  Base64.strict_encode64(masked_token) ⑤                                         # HTMLで使えるようBase64でエンコード
    # => "vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg=="
end

def real_csrf_token(session)
  session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH) ①     #=> "+srzK6xaej4ZKrn8xgt7ekGdjbzLMxa4NiBxf8QCmts="
  Base64.strict_decode64(session[:_csrf_token]) ②                                # => "\x95$\xBF\xBBj\x9B7\x03r\x14]\xEB\xF3\xF16,/\xECc\xCFE|I\x87\x82ly\xE6\xC0\x13\xBEp"
end

masked_authenticity_tokenの流れです。途中、共有鍵暗号方式であるワンタイムパッドを利用してトークンを暗号化しています。

  • base64文字列を生成し、セッションに格納
  • ② ①で生成した文字列をデコード(バイト列)
  • ③ ランダムなバイト列を生成し暗号鍵を用意
  • ④ ③の暗号鍵を利用して2のトークンをXOR操作で暗号化
  • ⑤ 暗号鍵と④の暗号化したトークンをセットでbase64方式にエンコード
  • ⑥ ⑤が"csrf-token"タグに格納

protect_from_forgery

上述のステップ4にあたります。

# Rails 5.1以前はapp/controllers/application_controller.rbに自動記述
# Rails 5.2以降はActionController::Baseで有効になっている

protect_from_forgery with: :exception

protect_from_forgeryではverify_authenticity_tokenが定義されています。これにより継承する全てのコントローラーに検証プロセスを挟むようになります。

def protect_from_forgery(options = {})
  options = options.reverse_merge(prepend: false)

  before_action :verify_authenticity_token, options 
end

verify_authenticity_tokenをきっかけに、リクエスト時に送信されたトークンと、セッションに格納されたトークンの比較を開始します。

def verify_authenticity_token
  !verified_request?
end

def verified_request?
  !protect_against_forgery? || (valid_request_origin? && any_authenticity_token_valid?)
end                                                      

上記のコードでみて欲しいのがany_authenticity_token_valid?です。リクエスト時に送られたトークンが存在する場合valid_authenticity_token?にセッションとトークンを渡します。

def any_authenticity_token_valid?
  request_authenticity_tokens.any? do |token|
    valid_authenticity_token?(session, token)
  end
end

valid_authenticity_token?ではmasked_authenticity_tokenの逆操作を行って復号し、トークンを比較します。

def valid_authenticity_token?(session, encoded_masked_token)
  masked_token = Base64.strict_decode64(encoded_masked_token) ①     # base64形式のトークンをdecode

  if masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
    csrf_token = unmask_token(masked_token)                           
    compare_with_real_token(csrf_token, session) ||  ④              # セッションのトークンと比較
      valid_per_form_csrf_token?(csrf_token, session)
  else
    false
  end
end

def unmask_token(masked_token)
  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) ③            # XORを取って、最終的な平文トークンを取得
end

def compare_with_real_token(token, session)
  ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
end

以下がvalid_authenticity_token?の流れです。

  • base64形式のトークンをデコード(バイト列)
  • ② ①のtokenを暗号鍵と暗号化済みトークンに分割
  • ③ 暗号鍵を利用して②のトークンをXOR操作で平文化(バイト列)
  • ④ セッションのトークンと比較

今回は、CSRFのおさらいとRailsCSRF保護機能がどのように実現されているのかを整理してみました。ですが、XSS対策がされていないと、これらのCSRF対策もすり抜けてしまいます。次回以降、XSS対策についても話していけたらなと思います。

参考文献一覧