注意
このエントリは急いで書いたので間違いが含まれている可能性が高いです。気づいた方はご指摘ください。
序文
strong_parameters とは、mass assignment で余計なパラメータをモデルの属性にセットさせないための新しい仕組みです。Rails 4.0 からはこれが標準になります。Rails2.x と 3.x はattr_accessible
やattr_protected
などで似たような機能が提供されていましたが、これだと管理が煩雑になるケースがありました。
今年の3月くらいにGitHub が mass assignment の脆弱性を突かれたことで 、この問題をどうにかしようという流れが起き、最終的に strong_parameters が作られるという経緯を辿りました。
mass assignment とは
Rails ではこんなコードをよく見ると思います。
def create
@user = User.new(params[:user])
unless @user.save
render 'new'
end
end
Userモデルにname
とemail
という属性が設定されていて、params[:user]
に{name: 'willnet', email: 'willnet@example.com'}
のようなパラメータが入っていたとき、上記のUser.new(params[:user])
で一度にname
とemail
が設定されます。これが mass assignment です。属性が多いときなど大変便利です。
mass assignment の問題点
先ほどの例ではname
とemail
という属性が設定されていました。この2つの属性の他に、システム側で自動で割り振られるtoken
という属性が存在したとします。token
はユーザには変更させたくありません。
さて、 それではUser#name
とUser#email
を変更するための update アクションを書いてみましょう。edit の view には name と email を変更するための text_field だけが存在するとします。
def update
@user = User.find(params[:id])
unless @user.update_attributes(params[:user])
render 'edit'
end
end
上記のコードは基本的にはうまく動くはずです。ただしユーザが普通にブラウザを使う限りは。
- chromeのデベロッパーツールなどで
<input type="text" name="user[token]" value="hoge" />
などのタグをform中に生成してsubmitする - curlやtelnetなどで直接
user[token]=hoge
のようなリクエストを、通常のリクエストに追加して送信する
上記のような方法を使った場合、ユーザに編集させたくないtoken
属性が更新されてしまいます。これが mass assignment の脆弱性と呼ばれるものです。
Rails2.x と 3.0系の mass assignment 脆弱性対策
attr_accessible
やattr_protected
が提供されていました。Rails2系(および3.0)だと後述のroleが設定できないため、例えばUser#name
だけ更新したい場合とUser#name
とUser#email
両方を更新させたい場合の2種類があるケースなど、同じモデルに複数種類の mass assignment 脆弱性対策をかけたくなります。ただこの頃はそのような機能が提供されておらず、そのため個人的にはこの頃のattr_accessible
やattr_protected
は全く使っていませんでした。そのころは更新させたい属性の配列を定義しておいて手動でparamsをフィルタリングするようなことをしていました。
Rails 3.1 以上からの mass assignment 脆弱性対策
attr_accessible
やattr_protected
に role という機能が導入されました。
class User < ActiveRecord::Base
attr_accessible :name
attr_accessible :name, :email, as: :name_and_email
end
as オプションで role の名前を指定して、
@user.update_attributes(params[:user], role: 'name_and_email')
のように role オプションで適用したいattr_accessible
(もしくはattr_protected
)を指定します。role がない場合はデフォルトのもの(上記のケースはattr_accessible :name
)が使用されます。
これにより大分きちんと書けるようになったのですが、多くの role を定義する必要があるとやや煩雑になります。
Rails 4.0 時代の mass assignment 脆弱性対策(strong_parameters)
Rails 4.0 からは基本の機能になるようですが、gem として使うことで 3.1 や 3.2 でも使うことができます。
導入
github上の README にも書いてありますが、普通にGemfile
に書いてbundle install
。あとは使用したいモデルで下記のモジュールをincludeすればOKです。
class User < ActiveRecord::Base
include ActiveModel::ForbiddenAttributesProtection
end
また、config/application.rb
に下記の設定が true になっている場合は false に変更します。
config.active_record.whitelist_attributes = false
使い方
下記のように使います。
def update
@user = User.find(params[:id])
permitted_params = params.require(:user).permit(:name, :email)
unless @user.update_attributes(permitted_params)
render 'edit'
end
end
permit の行をコメントアウトすると、ActiveModel::ForbiddenAttributes
の例外が発生します。permit メソッドの引数として指定された属性は mass assignment を許可されます。指定されなかった属性が params に入っている場合はフィルタリングされ、モデルには渡りません。
require メソッドは、そのパラメータがないと例外を吐くメソッドです。実際のコードを見た方が理解が早いかもしれません。
def require(key)
self[key].presence || raise(ActionController::ParameterMissing.new(key))
end
開発中のときなど、全ての属性を許可したいケースがあると思いますが、その時はparams[:user].permit!
のようにします。ただ、permit!
が今のバージョン(0.1.4)だと再帰的に効かないという話しがあるので(RailsCastsで言ってた。未確認)、うまくいかない場合は github 上の最新を使ってみてください。
nested parameters にも対応しているようです(未確認)。README をみたところ下記のような感じで書けるようです。
params.permit(:name, friends: [ :name, { family: [ :name ] }])
また、 strong_paramters を入れた状態で scaffold をしたら下記のアクションが生成されていました。なかなか気が利く感じですね。
# Use this method to whitelist the permissible parameters. Example:
# params.require(:person).permit(:name, :age)
# Also, you can specialize this method with per-user checking of permissible attributes.
def user_params
params.require(:user).permit(:email, :name, :token)
end
感想
attr_accessible
やattr_protected
よりもスマートに書けると思うので、Rails 3.1 と 3.2 を使っている人は導入してみると良いのではないでしょうか。