【Ruby on Rails】CSRF対策とStrong Parametersでセキュリティを守る

Ruby

こんにちは、かつコーチです。

「Railsはデフォルトで安全」とよく言われますが、その裏でどんな仕組みが動いているか説明できますか。

この記事では、protect_from_forgeryによるCSRF対策と、Strong Parametersの役割を仕組みから解説します。

普段何気なく使っている機能の意味を理解し、セキュリティ設定を自信を持って扱えるようになりましょう。

protect_from_forgeryの仕組み

CSRFとは何か

CSRF(Cross-Site Request Forgery)とは、悪意あるサイトを経由して、ユーザーの意図しないリクエストを別サイトに送らせる攻撃手法です。

たとえば、ログイン中のユーザーが悪意あるサイトを開くと、そのサイトに埋め込まれたフォームが自動送信されます。

送信先が銀行アプリなどであれば、ユーザーが気づかないうちに送金処理が実行されてしまう危険があります。

CSRFトークンによる防御

Railsは、この攻撃を防ぐためにCSRFトークン(リクエストごとに発行される検証用の文字列)を使います。

フォーム送信時にトークンを埋め込み、サーバー側で一致を確認する仕組みです。

Rails 8系のデフォルト構成では、ApplicationControllerに以下の記述があります。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

with: :exceptionを指定すると、トークンが不正な場合にActionController::InvalidAuthenticityToken例外が発生します。

ERBのフォームヘルパーを使っていれば、CSRFトークンは自動的に埋め込まれます。

<%# app/views/posts/_form.html.erb %>
<%= form_with model: @post do |f| %>
  <%= f.text_field :title %>
  <%= f.submit %>
<% end %>
<%# hidden fieldとしてauthenticity_tokenが自動出力される %>

生成されたHTMLを確認すると、<input type="hidden" name="authenticity_token" value="...">が挿入されているのが分かります。

API開発時の注意点

JSON APIとしてのみ利用するコントローラでは、CSRFトークンの検証方法を変える必要があります。

# app/controllers/api/base_controller.rb
class Api::BaseController < ActionController::API
  # ActionController::APIを継承していればCSRF保護は自動的に無効
end

ActionController::Baseを継承したままAPIを作る場合は、次のように分岐させます。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session, if: -> { request.format.json? }
end

セッションを使わないトークン認証のAPIであれば、CSRF保護そのものが不要になるケースもあります。

design上どちらの方針を取るか、事前に整理しておくとよいでしょう。

Strong Parametersの役割の再確認

なぜパラメータを許可制にするのか

Strong Parametersとは、コントローラで受け取るパラメータを明示的に許可する仕組みです。

これがなければ、フォームに存在しないパラメータをリクエストに紛れ込ませることで、意図しない属性を更新される危険があります。

たとえば、ユーザーが自分のadminフラグを勝手にtrueに書き換える、といった攻撃が可能になってしまいます。

permitとrequireの基本

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])
    @user.update(user_params)
    redirect_to @user
  end

  private

  def user_params
    params.require(:user).permit(:name, :email)
  end
end

require(:user)は、params[:user]が存在しない場合に例外を発生させます。

permit(:name, :email)は、許可した属性以外を自動的に除外します。

この2つを組み合わせることで、ホワイトリスト方式(許可した項目だけを通す方式)のパラメータ制御が実現できます。

よくあるつまずきポイント・エラー対処

ネストしたパラメータの許可漏れ

❌Before

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :body)
end

投稿にタグを複数紐付けるフォームを追加したところ、タグの選択結果が保存されませんでした。

原因は、ネストした配列パラメータの許可漏れでした。

✅After

# app/controllers/posts_controller.rb
def post_params
  params.require(:post).permit(:title, :body, tag_ids: [])
end

配列を受け取る場合はtag_ids: []のように、キーと空配列を組み合わせて許可する必要があります。

私が実際に遭遇したときは、フォーム側は正しく実装できていたので、原因の特定に時間がかかりました。

送信データをブラウザの開発者ツールで確認し、params.inspectをログ出力して初めて許可漏れに気づきました。

ActionController::InvalidAuthenticityTokenの対処

Ajaxで独自にPOSTリクエストを送る実装を追加した際、次のエラーに遭遇しました。

ActionController::InvalidAuthenticityToken

❌Before

// app/javascript/custom_post.js
fetch("/posts", {
  method: "POST",
  body: JSON.stringify({ title: "test" }),
  headers: { "Content-Type": "application/json" }
});

CSRFトークンをヘッダーに含めていなかったことが原因でした。

✅After

// app/javascript/custom_post.js
const token = document.querySelector('meta[name="csrf-token"]').content;

fetch("/posts", {
  method: "POST",
  body: JSON.stringify({ title: "test" }),
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-Token": token
  }
});

meta[name="csrf-token"]は、レイアウトの<head>内で<%= csrf_meta_tags %>により出力されています。

このトークンをリクエストヘッダーに含めることで、正常に処理が通るようになりました。

まとめ

この記事のポイント

  • CSRFはユーザーの意図しないリクエストを外部から送らせる攻撃で、Railsはトークン検証で防御している
  • protect_from_forgery with: :exceptionがRailsのデフォルト設定
  • APIコントローラでは、CSRF保護の要否をトークン認証方式に応じて判断する
  • Strong Parametersはrequirepermitのホワイトリスト方式で不正な属性更新を防ぐ
  • 配列やネストしたパラメータはtag_ids: []のような書き方で許可する

次に読むべき記事

  • コントローラの基本:アクションとストロングパラメータ
  • Deviseで本格的な認証機能を実装する

タグ: Ruby on Rails, 中級者向け, セキュリティ

タイトルとURLをコピーしました