はじめに
formクラスにparamsを渡してsuperで初期化しようとした際にActiveModel::ForbiddenAttributesErrorになる原因をこの記事で解明していこうと思います。
忙しい人向けの結論
paramsに対して.permit
していないからです。
formクラスにparamsを渡す前に、controller側でparams.permit(:〇〇)とするとエラーが起きなくなります。
よければその理由を以下の文章を通してより深く理解してみてください!
なぜForbiddenAttributesErrorになるのか?
コード例
コード例は以下のような状況を基に作成されています。
- 醤油ラーメンを注文しようとしている
- このラーメン屋では、醤油ラーメンと豚骨ラーメンしか売っていない
- 醤油ラーメン・豚骨ラーメン以外の注文が来ないようにバリデーションをかけたい
# 醤油ラーメンの注文がparametersで飛んでくる
#<ActionController::Parameters {"soup"=>"醤油"} permitted: false>
class OrderController
def order
form = OrderForm.new(params)
raise '醤油・豚骨以外は許さん!' if form.invalid?
form.order
end
end
class OrderForm
include ActiveModel::Model
validates :soup, inclusion: { in: ['醤油', '豚骨'] }
def initialize(attributes = {})
super(attributes)
end
def order
# 注文処理を行う
end
end
エラー詳細
処理を走らせると以下のようなエラーが出ました。
Rendering with exception: ActiveModel::ForbiddenAttributesError
/usr/local/bundle/gems/activemodel-6.1.7.7/lib/active_model/forbidden_attributes_protection.rb:23:in `sanitize_for_mass_assignment'
web-central-1 | /usr/local/bundle/gems/activemodel-6.1.7.7/lib/active_model/attribute_assignment.rb:34:in `assign_attributes'
エラーをよく見るとlib/active_model/forbidden_attributes_protection.rb
のsanitize_for_mass_assignment
メソッド内でエラーになっていることがわかります。
以下が該当ファイルです。
module ForbiddenAttributesProtection
private
# 今回attributesに入っている値は #<ActionController::Parameters {"soup"=>"醤油"} permitted: false>
def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?)
# permitted: falseだったらForbiddenAttributesErrorを起こす。
raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
attributes.to_h
else
attributes
end
end
alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end
エラーが起こる原因とエラーを起こしたい理由って?
エラーが起きている理由は上記コードブロック内に記載があるとおり、permitted: false
だからですね。
ではなぜpermitted: false
のときにエラーを起こす必要があるのでしょうか?
答えはマスアサインメントという機能にあります。
マスアサインメントって?
Railsガイド参照
チェックされていないパラメータをまるごとモデルに保存する行為は、モデルに対する「マスアサインメント」と呼ばれています。これが発生すると、正常なデータの中に悪意のあるデータが含まれてしまう可能性があります。
この文章だけ見るとよくわからないですよね。銀行口座開設を例に出して説明します。
(イメージしやすいように極端な例にしています)
銀行口座開設をする際に以下の要素をwebサイト上で入力して送信するとします。
- 氏名
- 生年月日
- 電話番号
普通の人は上記のみを入力して送信します。
ですが、悪い人はこのように考えます。
悪い人:「もしかしたら、貯金残高10億円というパラメーターを送信すれば、貯金残高10億円の銀行口座を作れるんじゃないか!?」
実はこれ可能なんです。 セキュリティがガバガバであれば貯金残高10億円の夢の銀行口座がつくれちゃいます。
このように悪意のあるデータが紛れ込んでしまうことをマスアサインメントといいます。
マスアサインメントを防ぐにはどうすればいいの?
答えは .permit
を使用する です。
.permit
を使用することで不正なパラメーターを防止できます。
.permit
を使用したコード例
元のコードを以下のように変更しました。
class OrderController
def order
# 元はform = OrderForm.new(params)
form = OrderForm.new(params.permit(:soup)
raise '醤油・豚骨以外は許さん!' if form.invalid?
form.order
end
end
変更点は.permit(:許可したいパラメーター)
を追加したことです。
これによってパラメーターが以下のようになりました。
#<ActionController::Parameters {"soup"=>"醤油"} permitted: true>
permitted: false
がpermitted: true
に変化しているのでエラーを起こすことなく処理を走らせることができるようになっています。
ただ、これだと.permit
のメリットが伝わらないと思うので、不正に煮卵をトッピングしてみましょう。
#<ActionController::Parameters {"soup"=>"醤油", "topping"=>"boiled egg"} permitted: false>
上記のようなパラメーターをorder_controller
に飛ばしてみましょう。
すると.permit
によって以下のようなパラメーターになります。
#<ActionController::Parameters {"soup"=>"醤油"} permitted: true>
"topping"=>"boiled egg"
というパラメーターが消ていますね。
このように.permit
を使用すると、指定したもの以外の不正なパラメーターを除外してくれ、安全に処理を実行できます
これで.permit
のメリットが理解できたと思いますので、ぜひ活用してみてください!
結論
- formクラスにparamsを渡してsuperで初期化しようとした際にActiveModel::ForbiddenAttributesErroとなる原因は
.permit
で飛んでくるパラメーターを許可していないから -
.permit
を使用すると、不正なパラメーターを防いでくれるため安全に処理を実行できる