protect_from_forgeryについて

こんにちは!oda@エンジニア1年目です!

以前の記事で、今後の課題としていたprotect_from_forgeryについて調べたので、今日はその内容について書きます。

調べる中で、コントローラーにprotect_from_forgeryを追加すればActionController::InvalidAuthenticityTokenを解消できるという情報をいくつか見ました。

試しに、コントローラーにprotect_from_forgeryを追加してみると、エラーは出ず、問題が解決されたように見えました。

ただ、これだけではなぜエラーが出なくなったのかわからないので、もう少し詳しく調べてみます。

CSRFとは?

まずは発生していたエラーをあらためて確認します。

Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 3ms (ActiveRecord: 0.0ms | Allocations: 468)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

CSRFトークンの信頼性が確認できないと言われていますが、そもそもCSRFとは何でしょうか。

CSRF(クロスサイトリクエストフォージェリ)とは、あるサイトにログインしている状態で、メールや別サイトに悪意を持って仕掛けられたリンクをクリックすると、ログイン中のサイトにリクエストが送られ、本人の意思とは違う操作をさせられるというような問題のことでした。

例をあげると、以下のようになります。

  • ユーザーが掲示板サイトAにログインする
  • サイトAにログインしたまま、悪意を持って仕掛けられた別サイトのリンクBをクリックする
    • リンクBには、サイトAに不適切な書き込みをするリクエストが埋め込まれている
  • サイトAはログイン中のユーザーからのリクエストなので、適切なリクエストとして処理する
  • ユーザーの意思とは関係なく、サイトAに不適切な書き込みがされる

CSRF対策として、Railsではビューの<head>タグ内にある<meta name="csrf-token" content="XXXXXXXXXXXX">contentが、リクエストに含まれているかチェックし、含まれていなければ上記のエラーが発生します。

この<meta name="csrf-token" content="XXXXXXXXXXXX">はそのサイト自身しか知らないため、別サイトからの不正なリクエストが防げるという仕組みです。

protect_from_forgeryについて

protect_from_forgeryは上記のCSRF対策として使われています。

protect_from_forgery with: :exceptionをコントローラーに追加することで有効にすることができますが、デフォルトで有効になっているので特に記載する必要はありません。(参考:Railsガイド

こちらが有効になっているため、前回の記事で試したようにauthenticity_tokenが含まれていないリクエストをするとエラーが発生してしまいました。

さて、冒頭で述べていた

コントローラー にprotect_from_forgeryを追加すればActionController::InvalidAuthenticityTokenを解消できるという情報をいくつか見ました

というのは何だったのでしょうか?

オプションを渡さずにprotect_from_forgeryを追加するということは、protect_from_forgery with: :null_sessionを追加することと同義のようです。(参考:Ruby on Rails API

protect_from_forgery with: :null_sessionauthenticity_tokenの検証がされていないリクエストを許可するという設定なので、私が見た情報はCSRF対策をしないことでエラーを回避するというものだったようです。(参考:Ruby on Rails API

この方法が必要な場面もあるかもしれませんが、私の目的はCSRF対策をしないことではないので、今回についてはこの解決方法は適切ではありません。

X-CSRF-Tokenヘッダにトークンを設定する

以前の記事を書いた後、社内で「X-CSRF-Tokenヘッダにトークンを設定する」という方法を教えていただきました。

次のとおりコードを修正して試してみると、こちらの方法でも同じように投稿内容を削除することができました。

function deletePost(post_id) {
    const token = $('meta[name="csrf-token"]').attr('content');
  $.ajax({
    url: `/posts/${post_id}`,
    // data: { authenticity_token: token }, 削除
    headers: {'X-CSRF-Token' : token }, // 追加
    type: 'DELETE',
    dataType: 'json',
  })
  .done((data) => {
    console.log('削除しました');
  })
  .fail((data) => {
    console.log('削除に失敗しました');
  })
};

あらためて、Railsガイドを読み直してみると、

Ajax呼び出しに他のライブラリを使う場合は、そのライブラリのAjax呼び出しのデフォルトのヘッダーにセキュリティトークンを追加する必要があります。

という記述がありましたので、この方法がオーソドックスな方法のようです。(参考:Railsガイド

最初に調べたときはしっかりと読めていませんでした。反省。

今回は、該当箇所にheadersをベタ書きしていますが、ajaxSetupメソッドを使うとデフォルトで設定することができそうです。

やればやるほど、試したいことが芋づる式に増えていきます。

ふたたびbutton_toメソッドに戻ってくる

さらにあれこれ調べていると、もともと使っていたbutton_toメソッドには:remoteというAjaxで処理ができるオプションがあることがわかりました。

さっそく、次のようにオプションを指定して試してみると、自分で書いたAjaxの処理と同じことが、いとも簡単に実現できました。

<%= button_to "削除する", post_path(@post), method: :delete, remote: true %>

さいごに

最終的には、button_toメソッドにremote: trueを指定するだけでやりたいことが実現できたという、あっけない結論となりました。

今回使った:remoteオプションについては、自分が使っていなかっただけで使われているコードは見たことがあったので、きちんと理解できていれば、あっという間に解決していたはずです。

ただ、寄り道をしたからこそ、CSRF対策についてや、そのためにRailsが裏側で何をしてくれているかということを調べるきっかけになりました。

Railsが裏側でうまく処理してくれていて、私が意識できていないことはまだまだあるはずです。

今後も手を動かす中で知識を増やし、エンジニアとしての基礎を固めて行きたいと思います!