1
0

More than 3 years have passed since last update.

【Rails】StrongParameterの使用は必須ですか?

Last updated at Posted at 2021-09-03

概要

StrongParameterはrails4から導入されたマスアサインメントを防ぐために導入された仕組みですが、使用が前提になっていると思っている方がいるようですが実際には必須ではありません。例えば、以下のようにStrongParameterからHashにして入力しても使うことができます。なぜこのように動作するかについてrailsのソースコードを見ながら説明します。

class Dog < ActiveRecord
end

params = ActionController::Parameters.new(name: 'tama', status:"sleepy")

// new with strong parameter
Dog.new(params.permit(:name, :status))

// new without strong parameter (hash)
Dog.new(name: params[:name], status: params[:status])

// new without strong parameter (hash)
Dog.new( {:name =>  params[:name], :status => params[:status]})

ソースは、タグv6.1.4.1を参照します。

ActiveRecordを初期化するとどこへ行くの?

ActiveModelのクラスをnewするとactive_record/core.rbで以下の初期化処理が走ります。 普段渡しているStrongParameterはここではattributesという変数で扱われ、assign_attibutesを呼び出すようになっています。

def initialize(attributes = nil)
  @new_record = true
  @attributes = self.class._default_attributes.deep_dup

  init_internals
  initialize_internals_callback

  assign_attributes(attributes) if attributes

  yield self if block_given?
  _run_initialize_callbacks
end

assign_attibutesって何してるの?

assign_attributesは、active_model/attribute_assignment.rbに定義されています。その名前の通り、ActiveModelの属性を設定するための処理が書かれています。

respond_to?はObjectクラスのメソッドで、指定した名前のメソッドがあるかどうかを返却します。また引数で指定しているstringify_keysは、active_suportのhashで定義されており、{ name: 'Rob', age: '28' }.stringify_keysとすると、{"name"=>"Rob", "age"=>"28"}のようにkeyを文字列にしたHashオブジェクトを返却します。 ここでは、new_attributesがstringify_keysメソッドが無ければArgumentErrorとし、有れば呼び出して、sanitize_for_mass_assignmentに結果を渡すという処理をやっています。

def assign_attributes(new_attributes)
  if !new_attributes.respond_to?(:stringify_keys)
    raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
  end
  return if new_attributes.empty?

  attributes = new_attributes.stringify_keys
  _assign_attributes(sanitize_for_mass_assignment(attributes))
end

assign_attributesの定義
https://github.com/rails/rails/blob/v6.1.4.1/activemodel/lib/active_model/attribute_assignment.rb#L28

stringify_keysの定義
https://github.com/rails/rails/blob/v6.1.4.1/activesupport/lib/active_support/core_ext/hash/keys.rb#L10

sanitize_for_mass_assignmentって何をやっているの?

attributesにpermitted?メソッドが存在するかどうかをrespond_to?を使って確認し、呼び出して許可されていなければForbiddenAttributesErrorとしています。そしてこれがマスアサインメントを防ぐための仕組みになります。 では、permitted?メソッドが存在しない場合はどうなるかというと、elseに書かれているようにattributesをそのまま返すようになっています。 要するに、前述したstringify_keysメソッドが定義されていることが満たされていれば、サニタイズは実行されませんので、表題の通りstrong parameterは必須ではないということになります。

module ForbiddenAttributesProtection # :nodoc:
  private
  def sanitize_for_mass_assignment(attributes)
    if attributes.respond_to?(:permitted?)
      raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
      attributes.to_h
    else
      attributes
    end
  end
  alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end

sanitize_for_mass_assignmentの定義
https://github.com/rails/rails/blob/v6.1.4.1/activemodel/lib/active_model/forbidden_attributes_protection.rb

この処理の流れだとpermitted?メソッドって呼び出せないんじゃないの?

assign_attributesメソッドでは、stringify_keysを呼び出してHashに変換してからsanitize_for_mass_assignmentを呼び出しています。Hashオブジェクトにはpermitted?メソッドが定義されていないから、結果的にサニタイズの処理が走らないんじゃないのか? と思えるかもしれません。

attributes = new_attributes.stringify_keys # ここで、ActionController::ParametersがHashになるのでは?
_assign_attributes(sanitize_for_mass_assignment(attributes))

Hashにはpermitted?は定義されてないので、何も対応しないと想像の通りの処理になってしまいます。そこで、StrongParameter(実体はActionController::Parameters)では、以下のようにstringify_keysを定義して自身のオブジェクトのシャローコピーを返すようにしています。これによって先ほどの行では、StrongParameter(ActionController::Parameters)が入力された時は、Hashには変換されずにActionController::Parametersが返却されるという仕組みとなっています。

    # This is required by ActiveModel attribute assignment, so that user can
    # pass +Parameters+ to a mass assignment methods in a model. It should not
    # matter as we are using +HashWithIndifferentAccess+ internally.
    def stringify_keys # :nodoc:
      dup
    end

strong_parameters.rb
https://github.com/rails/rails/blob/v6.1.4.1/actionpack/lib/action_controller/metal/strong_parameters.rb#L853

ブラックリスト方式のMyStrongParameterを作ってみる

permitted?とstringify_keysを定義すれば、StrongParameterとして動作させることができるとわかりましたので、独自にStrongParameterを作ってみました。既存のものはホワイトリストで、許可したいkeyのみを指定する方式なので、逆にブラックリスト方式にしてみます。

    class MyStrongParameter < Hash

      BLACK_LIST = [
          :id,
          :created_at,
          :updated_at
      ]

      def initialize(constructor = {})
        if constructor.respond_to?(:to_hash)
          super()
          update(constructor)

          hash = constructor.to_hash
          self.default = hash.default if hash.default
          self.default_proc = hash.default_proc if hash.default_proc
        else
          super(constructor)
        end
      end

      def stringify_keys
        dup
      end

      def permitted?
        !keys.any? {|key| BLACK_LIST.include? key }
      end
    end

実際のStrongParameterはHashWithInDifferentAccessを内包するクラスですが、ここでは簡単にするためサブクラスとして実装します。stringify_keysは既存のものと同様に自身のシャローコピーを返却するようにして、permitted?ではブラックリストに1つでも含まれていれば許可しないようにロジックを書きます。 これを実行すると以下のようになります。

irb(main):213:0> MyStrongParameter.new(name: "kuro", status: "sleepy").permitted?
=> true
irb(main):214:0> MyStrongParameter.new("name": "kuro", "status": "sleepy").permitted?
=> true
irb(main):215:0> MyStrongParameter.new(id: "kuro", status: "sleepy").permitted?
=> false
irb(main):216:0> MyStrongParameter.new(id: 1, name: "kuro", status: "sleepy").permitted?
=> false
irb(main):217:0> MyStrongParameter.new(name: "kuro", status: "sleepy", created_at: Time.now).permitted?
=> false

このクラスで実装したオブジェクトを使って、実際にマスアサインメントでサニタイズされるかどうかを確認します。

# ActiveModel(Dog)を定義
class Dog
      include ActiveModel::Model
      attr_accessor :name, :status
end

# ブラックリストに登録されているkeyは使ってないので、permitted?はtrueとなり正常に犬(タマ)ができる
dog.assign_attributes(MyStrongParameter.new(name: "tama", status: "sleepy"))
=> {:name=>"tama", :status=>"sleepy"}


# idはブラックリストに登録されているためpermitted?はfalseとなりサニタイズされる(ForbiddenAttributesErrorが出る)
dog.assign_attributes(MyStrongParameter.new(id: 1, name: "tama", status: "sleepy"))
Traceback (most recent call last):
        1: from (irb):261
ActiveModel::ForbiddenAttributesError (ActiveModel::ForbiddenAttributesError)

良い感じに動作しています。

まとめ

StrongParameterは必須ではないので使わないという選択もできます。
ActiveModelの内部ではStrongParameterのインスタンスであるかというチェックをしているわけでは無いので拡張も可能です。

1
0
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
1
0