selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

react-railsを利用したRailsアプリケーションにてCSRFの対策を行う

はじめに

最近久しぶりに Rails で Web アプリケーションを開発しました。その中で React でフォームを作ることになったため、CSRF に関する対策について調べました。そのとき調べた内容を記載します。

なお、React の利用は SPA などではなく、react-rails を利用してページごとに React Component を読み込ませています。

CSRF について

CSRF (Cross Site Request Forgeries) とは、悪意のあるユーザーが、任意の Web アプリケーションを利用しているユーザーの認証情報を用いて、任意の Web アプリケーション上の機密情報を盗む、または意図しないコードの実行をしようとする試みのことです。

具体的な攻撃方法について、Rails ガイドに記載されている例を元に説明します。

<img
  src="http://www.webapp.com/project/1/destroy"
  width="0"
  heigth="0"
  border="0"
/>

上記のような img tag が含まれたページをブラウザが表示するとき、ブラウザは http://www.webapp.com/project/1/destroy に対して GET リクエストを実行します。

もし攻撃を受けた側のユーザーが www.webapp.com のサービスを利用しており、 web.webapp.comの認証情報がセッションや Cookies に残っていた場合、その認証情報を利用して GET http://www.webapp.com/project/1/destroyAPI を実行することができます。

JavaScript を利用すれば、POST リクエストも行うことができます。次のように作られたリンクをクリックすると、POST http://www.example.com/account/destroy が実行されてしまいます。

<a
  href="http://www.harmless.com/"
  onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;"
  >To the harmless survey</a
>

Rails における基本的な CSRF 対策について

Rails における CSRF の対策は、基本的には authenticity_token というパラメータを form 毎に埋め込み、POST リクエストの中でその token を検証するというものです。次のような形で、form に authenticity_token というパラメータを埋め込みます。そして、これと同じトークンをセッションにも保存します。

<form action="/login" accept-charset="UTF-8" method="post">
  <input type="hidden" name="authenticity_token" value="xxxxx==" />>
  <input type="email" name="sessions[email]" id="sessions_email" />
  ...
  <input type="submit" name="commit" value="OK" data-disable-with="OK" />
</form>

token の検証方法について、rails のドキュメントでは下記のように記載されています。

Returns true or false if a request is verified. Checks:

・Is it a GET or HEAD request? GETs should be safe and idempotent
・Does the form_authenticity_token match the given token value from the params?
・Does the X-CSRF-Token header match the form_authenticity_token?

実際の Rails のコードと合わせると、下記のような条件を満たしたとき token の検証は問題ないと判断されます。

  • request が get/ head である
  • CSRF の保護が有効になっており、かつ下記の条件を満たしている
    • request の origin が nil である。もしくは、同一 origin からのリクエストである
    • 下記パラメータのいずれかが、セッション内に格納されている authenticity_token と一致している
      • authenticity_token パラメータ
      • request.x_csrf_token

CSRF の保護は test 環境以外ではデフォルトで有効になっています。そのため、通常の Web アプリケーションの利用においては、GET リクエストでリソースの更新等をしていない限り、別段意識せずとも Rails 側が対応してくれることになります。

action/method毎に トークンの設定をする

ただし、CSP(Content Security Policy) を利用しているサービスにおいては追加で対応が必要となります。下記のような HTML が渡されたとき、後者の /innocuous にリクエストをする form 要素は無視され、前者の /user/change_password の方が優先されると報告されています。これにより、ユーザーのパスワードを変更するなどの攻撃が可能となります。

<form method="post" action="/user/change_password">
  <!-- xss -->
  <form method="post" action="/innocuous">
    <input type="hidden" name="authenticity_token" value="thetoken" />
  </form>
</form>

この問題は、この Pull Request において言及され、対策が取り込まれました。対策内容はaction/method ごとに authenticity_token を作成するというものです。これにより、上述の form hijacking により生成された POST リクエストは無効となります。

今回の対処方法

今回 CSRF 対策をする上で行ったことは下記2点です。

  • config/application.rb にて per_form_csrf_tokensを true とする (参考)
  • React Component の引数に action, method を指定した authenticity_token を渡す

authenticity_tokenの渡し方については下記のようになります。

# app/views/sessions/new.html.erb
<%= react_component("LoginForm", {
  loginUrl: admin_password_reset_url,
  token: form_authenticity_token(form_options: { action: session_path, method: "post" }),
}) %>

最後に

今回 SPAではないReactを利用したRailsアプリケーションで、CSRFの対策を行う方法についてまとめました。 この方法の問題点や、もっと良い実装方法がありましたら、コメント等をもらえると嬉しいです!