はじめに
最近久しぶりに 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/destroy
の API を実行することができます。
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の対策を行う方法についてまとめました。
この方法の問題点や、もっと良い実装方法がありましたら、コメント等をもらえると嬉しいです!