目次
こんにちは、キラメックスでテックアカデミーの開発をしている久保田です。今回はCSRFのおさらいとRailsのCSRF保護機能がどのように実現されているのかコードリーディングしつつ整理してみました。
CSRFとは?
- ウェブアプリケーションの脆弱性を悪用したサイバー攻撃
- サイト横断的に(Cross Site)リクエストを偽装(Request Forgeries)すること
安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ):IPA 独立行政法人 情報処理推進機構
具体的な被害
- アカウントの削除
- パスワード変更によるアカウント乗っ取り
- データの更新・削除
実際に起きた事件・騒動
横浜市の小学校襲撃予告容疑、片山被告を追送検: 日本経済新聞
- mixiの「はまちちゃん」騒動
ソーシャル・ネットワーキングサイトの「mixi」で、URLをクリックすると勝手に「ぼくはまちちゃん!」というタイトルで日記がアップされてしまうという現象が多発した
大量の「はまちちゃん」を生み出したCSRFの脆弱性とは? - ITmedia エンタープライズ
対策概要(ワンタイムトークン方式)
ワンタイムトークンを用いた対策では、4つのステップで構成されています。
- レンダリング時に一意のトークンをHTMLに埋め込む
- これと同じトークンをセッションに保存
- ユーザーのPOSTリクエスト時に、埋められていたトークンも一緒に送信
- 送信されたトークンとセッションのトークンを比較し、一致するかを確認
RailsのCSRF保護機能
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のおさらいとRailsのCSRF保護機能がどのように実現されているのかを整理してみました。ですが、XSS対策がされていないと、これらのCSRF対策もすり抜けてしまいます。次回以降、XSS対策についても話していけたらなと思います。