おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

Rails4.0に含まれる strong_parameters について

注意

このエントリは急いで書いたので間違いが含まれている可能性が高いです。気づいた方はご指摘ください。

序文

strong_parameters とは、mass assignment で余計なパラメータをモデルの属性にセットさせないための新しい仕組みです。Rails 4.0 からはこれが標準になります。Rails2.x と 3.x はattr_accessibleattr_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モデルにnameemailという属性が設定されていて、params[:user]{name: 'willnet', email: 'willnet@example.com'}のようなパラメータが入っていたとき、上記のUser.new(params[:user])で一度にnameemailが設定されます。これが mass assignment です。属性が多いときなど大変便利です。

mass assignment の問題点

先ほどの例ではnameemailという属性が設定されていました。この2つの属性の他に、システム側で自動で割り振られるtokenという属性が存在したとします。tokenはユーザには変更させたくありません。

さて、 それではUser#nameUser#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_accessibleattr_protectedが提供されていました。Rails2系(および3.0)だと後述のroleが設定できないため、例えばUser#nameだけ更新したい場合とUser#nameUser#email両方を更新させたい場合の2種類があるケースなど、同じモデルに複数種類の mass assignment 脆弱性対策をかけたくなります。ただこの頃はそのような機能が提供されておらず、そのため個人的にはこの頃のattr_accessibleattr_protectedは全く使っていませんでした。そのころは更新させたい属性の配列を定義しておいて手動でparamsをフィルタリングするようなことをしていました。

Rails 3.1 以上からの mass assignment 脆弱性対策

attr_accessibleattr_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_accessibleattr_protectedよりもスマートに書けると思うので、Rails 3.1 と 3.2 を使っている人は導入してみると良いのではないでしょうか。