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

## はじめに

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

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

※ この記事は[自分の個人ブログ](https://selmertsx.hatenablog.com/)からの転載です。

## CSRF について

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

具体的な攻撃方法について、[Rails ガイド](https://railsguides.jp/security.html#%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%83%95%E3%82%A9%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%AA-csrf)に記載されている例を元に説明します。

```html
<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` が実行されてしまいます。

```html
<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 というパラメータを埋め込みます。そして、これと同じトークンをセッションにも保存します。

```html
<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 のドキュメント](https://api.rubyonrails.org/v6.0/classes/ActionController/RequestForgeryProtection.html#method-i-verified_request-3F)では下記のように記載されています。

```
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 のコード](https://github.com/rails/rails/blob/v6.0.2/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L233-L246)と合わせると、下記のような条件を満たしたとき token の検証は問題ないと判断されます。

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

[CSRF の保護は test 環境以外ではデフォルトで有効](https://railsguides.jp/configuring.html#action-controller%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)になっています。そのため、通常の Web アプリケーションの利用においては、GET リクエストでリソースの更新等をしていない限り、別段意識せずとも Rails 側が対応してくれることになります。

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

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

```html
<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](https://github.com/rails/rails/pull/22275) において言及され、対策が取り込まれました。対策内容はaction/method ごとに authenticity_token を作成するというものです。これにより、上述の form hijacking により生成された POST リクエストは無効となります。

## 今回の対処方法

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

- config/application.rb にて `per_form_csrf_tokens`を true とする ([参考](https://railsguides.jp/configuring.html#action-controller%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B))
- React Component の引数に action, method を指定した authenticity_token を渡す

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

```ruby
# 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の対策を行う方法についてまとめました。
この方法の問題点や、もっと良い実装方法がありましたら、コメント等をもらえると嬉しいです！