9
1

【Rails】formオブジェクトでForbiddenAttributesErrorが起こる原因と対処方法

Posted at

はじめに

formクラスにparamsを渡してsuperで初期化しようとした際にActiveModel::ForbiddenAttributesErrorになる原因をこの記事で解明していこうと思います。

忙しい人向けの結論

paramsに対して.permitしていないからです。
formクラスにparamsを渡す前に、controller側でparams.permit(:〇〇)とするとエラーが起きなくなります。
よければその理由を以下の文章を通してより深く理解してみてください!

なぜForbiddenAttributesErrorになるのか?

コード例

コード例は以下のような状況を基に作成されています。

  • 醤油ラーメンを注文しようとしている
  • このラーメン屋では、醤油ラーメンと豚骨ラーメンしか売っていない
  • 醤油ラーメン・豚骨ラーメン以外の注文が来ないようにバリデーションをかけたい
# 醤油ラーメンの注文がparametersで飛んでくる
#<ActionController::Parameters {"soup"=>"醤油"} permitted: false>
order_controller.rb
class OrderController
    def order
        form = OrderForm.new(params)
        raise '醤油・豚骨以外は許さん!' if form.invalid?
        
        form.order
    end
end
order_form.rb
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.rbsanitize_for_mass_assignmentメソッド内でエラーになっていることがわかります。
以下が該当ファイルです。

forbidden_attributes_protection.rb
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を使用したコード例

元のコードを以下のように変更しました。

order_controller.rb
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: falsepermitted: 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を使用すると、不正なパラメーターを防いでくれるため安全に処理を実行できる
9
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
1