これがどういう問題なのか知らない人は、
このブログ
http://blog.sorah.jp/2012/03/05/mass-assignment-vulnerability-in-github
か
Rails Cast - Hackers Love Mass Assignment
http://railscasts.com/episodes/26-hackers-love-mass-assignment-revised
(プロを見れない人:http://railscasts.com/episodes/26-hackers-love-mass-assignment)
を見ると良い。Rails Castの方では実際にAttackする方法が動画で見られるのでわかりやすい。
公式ドキュメントとしては、以下のページにその内容が書かれている。
http://guides.rubyonrails.org/security.html#mass-assignment
基本的にはattr_accessibleに書いてあるフィールドは、たとえFormで隠していようとも、何らかの方法でうまくパラメータをセットしてリクエストを送ってあげれば、書き換える事が可能ですよという話。
ここまでは気をつけましょうという話でいいのだけれど、じゃぁどういう風に解決すればいいのかが問題。これぞって言うベスト・プラクティスがまだない感じ。
対策1: mass_assignmentを最低限にし、後は自前で代入
基本中の基本。
- attr_accessibleは悪意を持って変更されてしまっても困らないものにする
- Association関係はなるべくActiveRecord::Relationから生成
- adminのみできる操作はuserがadminであるかチェックしてから直代入
- findするときに、current_userに紐付いているものだけから探す
といったことをして、対策する。
class ArticleController < ApplicationController
  def create
    @article = current_user.articles.new(params[:article])
    @article.pick_up = true if current_user.adimn? && params[:pick_up]
    if @article.save
      redirect_to action: :index, notice: "Created"
    else
      render :new
    end
  end
  def update
    @article = current_user.articles.find(params[:id])
    show_access_denied_page unless @article
    @article.pick_up = true if current_user.adimn? && params[:pick_up]
    @article.assign_attributes(params[:article])
    if @article.save
      redirect_to action: :index, notice: "Updated"
    else
      render :edit
    end
  end
end
基本はこれでいい気もするんだけど、Railsに置いてめんどくさいのは悪なのでどうしようかと言う話が出てくる。
対策2: asを使う
attr_accessibleにはasが付けられ、
class Product < ActiveRecord::Base
  attr_accessible :name, as: :user 
  attr_accessible :name, :pick_up, as: :admin
end
newやupdate_attributesの時にasを指定することが出来る。
class ProductController < ApplicationController
  def create
    @product = Product.new(params[:product], as: current_user.try(:admin?) ? :admin : :user)
    if @product.save
      redirect_to action: :index, notice: "Created"
    else
      render :new
    end
  end
  def update
    @product = Product.find(params[:id])
    if @product.update_attributes(params[:product], as: current_user.try(:admin?) ? :admin : :user)
      redirect_to action: :index, notice: "Updated"
    else
      render :edit
    end
  end
end
attr_accessibleの定義をuser/adminで分ける方法の他にも、new/editで分けるなどすればある程度応用が効きそう。
対策3: mass_assignment_authorizerを使う
mass_assignment_authorizer(公式ドキュメント)を使ってattr_accessibleをControllerで動的に書き換える方法。
Rails Castのコードスニペットをそのまま持ってくると、
class ActiveRecord::Base
  attr_accessible
  attr_accessor :accessible
  
  private
  
  def mass_assignment_authorizer
    if accessible == :all
      self.class.protected_attributes
    else
      super + (accessible || [])
    end
  end
end
と設定しておいて、adminだけなんでも設定できるようにする例では
def create
  @article = Article.new
  @article.accessible = :all if admin?
  @article.attributes = params[:article]
  // ...
end
def update
  @article = Article.find(params[:id])
  @article.accessible = :all if admin?  # <- here
  // ...
end
のようにする。
対策4: strong-parameters gemを使う
https://github.com/rails/strong_parameters
http://railscasts.com/episodes/371-strong-parameters
Rails4.0からはデフォルトになるらしい。
詳しくは上の2のドキュメントを見るほうが良いと思うので、ここではコードスニペットを書かないが、許可(permit)するattributeをテンプレート化しておき、その場その場で適用するイメージ。
おわりに
他にもこうするといいよというのがあったら教えて下さい。
