こんにちは、かつコーチです。
「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は
requireとpermitのホワイトリスト方式で不正な属性更新を防ぐ - 配列やネストしたパラメータは
tag_ids: []のような書き方で許可する
次に読むべき記事
- コントローラの基本:アクションとストロングパラメータ
- Deviseで本格的な認証機能を実装する
タグ: Ruby on Rails, 中級者向け, セキュリティ