« Rails3 to Rails4 変更点まとめ:2 / 4 »

カテゴリー:
Rails
タグ:
 Rails4 StrongParameters

このエントリーをはてなブックマークに追加
更新日時:
2014年02月26日(水)
作成日時:
2013年09月09日(月)

前の記事 / 次の記事

目次

そもそも何なのか?

例えば次のようなカラムを持つArticleテーブル(記事テーブル)があって、
更新時には headline と content だけを更新して user_id は変更したくない場合。
(user_id は 記事の作成者を識別するidとして使う。)

class CreateArticles < ActiveRecord::Migration
  def change
    create_table :articles do |t|
      t.integer :user_id
      t.string  :headline
      t.string  :content

      t.timestamps
    end
  end
end

次のようなビューを用意して

-# haml
-# user_id は更新の対象にしていない
=form_for @article do |f|
  %p
    =f.label :headline
    =f.text_field :headline
  %p
    =f.label :content
    =f.text_area :content
  %p
    =f.submit

コントローラーで次のような処理をすると、

class ArticleController < ApplicationController
  def update
    @article = Article.find(params[:id])

    if @article.update(params[:article])
      redirect_to @article
    else
      render action: "new"
    end
  end
end

このビューから送られて来たデータでは、コントローラーは user_id を更新しないが、

改編されたビューやビューを経由しないアクセスで、
user_id の存在するデータが送られて来た場合、コントローラーは user_id を更新してしまう。

つまり、記事の作成者を勝手に別のユーザーに変更できてしまうということになる。

この脆弱性をMass Assignment脆弱性といい、これを防ぐのが、StrongParameters。

どうやって使うのか?

Rails4でscaffoldするとStrong Parameters が適用されたコントローラーが生成されるので、
とりあえずそれを見てみるのが分かりやすいと思う。

基本

params.require(key).permit(filter)
# key
#   Strong Parameters を適用したい params の key を指定する。
# filter
#   Strong Parameters で許可するカラムを指定する。

params の内容が次の時、

{ 
  "article" => { 
    "user_id" => "99", "headline" => "最近のできごと", "content" => "エオルゼアが面白い" 
  }
}

指定したカラムだけが通る

article_params = params.require(:article).permit(:headline, :content)

article_params[:user_id]  #=>  nil
article_params[:headline] #=>  "最近のできごと"
article_params[:content]  #=>  "エオルゼアが面白い"

最初の例に適用する場合次のように書くことで、
仮に user_id のあるデータが渡されても user_id は更新されない。

class ArticleController < ApplicationController
  def update
    @article = Article.find(params[:id])

    if @article.update(params.require(:article).permit(:headline, :content))
      redirect_to @article
    else
      render action: "new"
    end
  end
end

ネストされたパラメーターに適用する

パラメーターがネストされている場合は、Strong Parameters もネストさせる。

モデルの定義が次のような時に、

class Article < ActiveRecord::Base
  has_many :comments
  accepts_nextsd_attributes_for :comments
end

次のような params にStrongParametersを適用したい場合、

{ 
  "article" => {
    "user_id"  => "99", "headline" => "最近のできごと", "content" => "エオルゼアが面白い", 
    "comments_attributes" => { "0" => { "user_id" => "9999", "headline" => "へー", "content" => "そうなんだ" } }
  }
}

次のように書ける
(※ 2014/02/26 ネストされた部分の出力結果の記述が間違っていたので修正)

article_params = params.require(:article).permit(:headline, :content, comments_attributes: [:headline, :content])

article_params[:user_id]            #=>  nil
article_params[:headline]           #=>  "最近のできごと"
article_params[:content]            #=>  "エオルゼアが面白い"
article_params[:comments][0][:user_id]  #=>  nil
article_params[:comments][0][:headline] #=>  "へー"
article_params[:comments][0][:content]  #=>  "そうなんだ"

デフォルト値を設定する

Strong Parameterを使った時に、対象のパラメーターが存在していないと、
エラーになる(ActionController::ParameterMissing)

例えば、

params.require(:article).permit(:headline, :content)

と書いた時、require で指定した

params[:article]

がないとエラーになる。

アプリケーションの仕様によっては、ここでエラーを出したくないケースというのがあると思う。
その時は、Strong Parameters の対象に対してデフォルト値を設定できる。

次のように fetch を使う。

params.fetch(:article, {}).permit(:headline, :content)
# params[:article] があればその値を、なければ {} として評価される。

fetch の第二引数を省略した場合も、値が存在していなければ ActionController::ParameterMissing になる。

嵌まらないようにする(エラーをraiseさせる)

Rails3以前に慣れていると、Strong Parameters なる存在を忘れていて、よく嵌まる。

で、無駄に色んなことを調べて「おかしい!なにも間違ってない!俺は間違ってない!!」
って思った頃にStrong Parameters の存在を思い出して「あーこれ何回目かな・・」って思う。

なので使い始めはエラーが上がるようにしておくのをお勧めしたい。

全ての環境(開発、テスト、本番)でエラーを上げたいなら、"config/application.rb" に、
開発環境でだけエラーを上げたいなら、"config/environments/development.rb" に、

次の行を追加する。

config.action_controller.action_on_unpermitted_parameters = :raise

こうすることで、許可されていないパラメーターが渡された時に、
ActionController::UnpermittedParameters エラーが上がるようになる。

仮想属性でも定義が必要

実際にDBに保存することのない仮想的な属性であっても
StrongParametersの定義がないと使用することはできない。

新しいオブジェクトをnewする時にStrongParametersが適用されるんだから
当然のことなんだけど、漫然と使ってるとそんなこと忘却して嵌まる。

例えば次のような処理で、実際に confirmed をDBに保存することがなくても、
StrongParametersに confirmed が定義されていないと @paper に confirmed の値を渡すことはできない。

class Paper < ActiveRecord::Base
  # この属性は保存しない
  attr_accessor :confirmed

  def create
    @paper = Paper.new(paper_params)

    # confirmed がなければバリデーションでエラー
    if @paper.save
      redirect_to @paper, notice: "failed."
    else
      render action: "new"
    end
  end

  private

    def paper_params
      params.require(:paper).permit(:confirmed)
    end

end

参考