RailsにおけるMassAssignmentのセキュリティー対策


これがどういう問題なのか知らない人は、

このブログ
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を使う

http://railscasts.com/episodes/237-dynamic-attr-accessible

mass_assignment_authorizer([公式ドキュメント](
http://api.rubyonrails.org/classes/ActiveModel/MassAssignmentSecurity.html#method-i-mass_assignment_authorizer))を使ってattr_accessibleをControllerで動的に書き換える方法。

Rails Castのコードスニペットをそのまま持ってくると、

```rb:config/initializers/accessible_attributes.rb
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だけなんでも設定できるようにする例では

```rb:app/controllers/articles_controller.rb
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をテンプレート化しておき、その場その場で適用するイメージ。


## おわりに
他にもこうするといいよというのがあったら教えて下さい。