Rails2のCSRF対策はレガシーなAPを保護しない

Rails-2.0がリリースされて半年が過ぎ、既にRails-2.1もリリースされているというのにRubricksはいまだにRails-1.2.6で動いている。というのも、Rubricksが使っているComponentsの仕組みそのものがRails2で消失してしまったため。

他の数多のPluginのように外出しされたとかなら兎も角、完全に無くなってしまったのは痛い。かといって、泣き言ばっかり言ってても仕方ないので最近重い腰をあげてRails-2.1のソースを読み込んでます。

Rails2の特徴の1つとして、「セキュリティの向上」が謳われている。その内容は主に以下の2点である。

  1. CSRF対策
  2. sanitizeの方式がホワイトリスト方式に変更

この2点、Rubricks Projectで提供しているrails_protection pluginと完璧に被るので、まずはこの周辺から読み込んでみる。


Rails2で追加されたCSRF対策(request_forgery_protection)の特徴は以下の通りとなっている。

  • ランダムな文字列を自動生成し、セッションに保持する
    • SecureRandomを利用して生成する
    • SecureRandomが利用できない場合はMD5のダイジェスト値を利用する
    • :secretオプションで独自に文字列を指定することが可能
  • 上記文字列のダイジェスト値をトークンとしてform_tagで自動的に付与する
  • コントローラで宣言したアクションの処理時に上記トークンの照合を行う
    • 例外としてGETリクエストに関しては照合を行わない


さて、ここまで読んだところでrequest_forgery_protectionには大きく3つの欠点があるように感じる。

  1. GETリクエストをスルーしているため根本的にCSRF対策になっていない
  2. 宣言したアクションだけにしか適用されないので開発者の宣言漏れの可能性がつきまとう
  3. FORMタグを使う部分は問題ないが、JavaScriptAJAXリクエストを発行する時は自前でトークンを付け加えてあげないといけない

1のGETリクエストをスルーする仕様に関しては、かなり致命的な問題である。例えば、以下のようなコントローラがあったとする。

==[/app/controllers/index_controller.rb]====
class IndexController < ApplicationController
  def index
  end

  def show
  end

  def update
    People.update(params[:id], params[:people])
    redirect_to :action => 'index'
  end
end

==[/config/routes.rb]====
ActionController::Routing::Routes.draw do |map|
  map.connect ':controller/:action/:id'
end

このようなAPに対して攻撃者が以下のような攻撃サイトを用意して、それを正規ユーザが踏めばCSRF攻撃が成立する。

<form action="http://xxx.com/index/update/1" method="GET">
  <input type="hidden" name="people[c1]" value="csrf" />
  <input type="hidden" name="people[c2]" value="csrf" />
  <input type="submit" value="ここをクリック!">
</form>

Rails開発陣がどのような意図でGETリクエストをスルーする仕様にしたのか、確かなところはわからないが、Rails2ではRESTfulなAP以外を想定していないのではないかと個人的には思う。要はActiveResorce使えよ、ってことね。ActiveResorceを前提とすると先のコードは以下のようになる。

==[/app/controllers/people_controller.rb]====
class PeopleController < ApplicationController
  def index
  end

  def show
  end

  def update
    People.update(params[:id], params[:people])
    redirect_to :action => 'index'
  end
end

==[/config/routes.rb]====
ActionController::Routing::Routes.draw do |map|
  map.resources :people
end

このようなAPの場合、#GET /people/1 で参照、 #POST /people/1 で更新となるため、GETリクエストで更新処理を試行することができない。このため「request_forgery_protection」は正常に機能し、CSRF攻撃からAPを保護することができる。

つまり、「request_forgery_protection」はRails2での基本思想である「RESTful」に準拠するAPは保護するけど、「map.connect」を使ってるようなレガシーなAPは保護しませんよーという事なのだと思う。

では、レガシーなAPをCSRF攻撃から守る機能をFW的に提供するためにはどうすればいいか。rails_protection pluginでは以下のような考えで保護機構を提供している。

  • 更新系のリクエスト(POST/PUT/DELETE)はトークンの照合を行う
  • 参照系のリクエスト(GET/HEAD/OPTION等)はDB更新を禁止する
    • リクエストに関わる処理でAR#saveもしくはAR#destroyが叩かれると例外を発生する
    • GETリクエストでも更新処理が必要な場合のためにARオブジェクトにフラグをセットすることで例外の発生を回避可能


参照系のリクエストにおいて、基本的にDB更新処理を禁止することによって、上述のようなGETリクエストを利用したCSRF攻撃を回避することができる。


長くなったので2・3の論点に関しては後で書く。