はじめに
最近久しぶりに 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 の保護が有効になっており、かつ下記の条件を満たしている
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の対策を行う方法についてまとめました。 この方法の問題点や、もっと良い実装方法がありましたら、コメント等をもらえると嬉しいです!