雑記

2000|01|
2003|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|
2007|01|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|09|11|
2009|02|03|05|06|07|08|10|11|12|
2010|01|03|04|05|06|07|08|09|10|
2011|05|06|09|10|
2012|03|07|09|12|
2013|01|02|04|05|07|08|10|11|
2014|04|05|08|10|12|
2015|01|05|
2016|09|

2010-09-29 [長年日記]

[Ruby][Sinatra] SinatraでRack::Csrfの例外をハンドリングする

SinatraでのCSRF対策にはRack::Csrfが使えますが、普通に

use Rack::Csrf, :raise => true

とか書いただけでは、errorハンドラで例外を捕捉・処理できません。production環境ではCSRFチェックに引っかかると403応答+真っ白な画面になるのですが、false negativeの可能性が0ではないので、何らかの処理はしておきたいところです。

というわけで、試行錯誤の結果行き着いたのが以下のようなコード。もっと良い方法があったらコメントください。

require 'rubygems'
require 'sinatra'
require 'rack/csrf'
require 'haml'

use Rack::Session::Pool
use Rack::Csrf, :raise => true, :skip => ['POST:.*', 'PUT:.*', 'DELETE:.*']

set :run, true
set :root, File.dirname(__FILE__)
set :show_exceptions, false

def check_csrf
   raise Rack::Csrf::InvalidCsrfToken unless params[Rack::Csrf.csrf_field] == session['csrf.token']
end

before do
   check_csrf if ['POST', 'PUT', 'DELETE'].include?(env['REQUEST_METHOD'])
end

error Rack::Csrf::InvalidCsrfToken do
   "CSRF exception!!"
end

##########

get '/?' do
   haml ROOT_HAML
end

get '/form' do
   haml FORM_HAML
end

get '/form2' do
   haml FORM_HAML_WITHOUT_CSRF_TAG
end

post '/?' do
   "ok"
end

##########

ROOT_HAML = <<EOHAML
%a{:href => "/form"} form
%br
%a{:href => "/form2"} form2
EOHAML

FORM_HAML = <<EOHAML
%h1 フォーム
%form{:method => "POST", :action => '/'}
  = Rack::Csrf.csrf_tag(env)
  %p
    %input{:type => "submit"}
EOHAML

FORM_HAML_WITHOUT_CSRF_TAG = <<EOHAML
%h1 フォーム
%form{:method => "POST", :action => '/'}
  %p
    %input{:type => "submit"}
EOHAML

上記ソースをstart.rbなど適当な名前で保存し、

% ruby start.rb

で起動し、http://sample-host:4567/ にアクセスすると、"form","form2"という2つのリンクが表示されます。"form"に進んでボタンを押すと"ok"、"form2"に進んでボタンを押すと"CSRF exception!!"と表示されます。

以下、ソースの要点を解説します。

読み込みと設定

require 'rack/csrf'
  :
use Rack::Csrf, :raise => true, :skip => ['POST:.*', 'PUT:.*', 'DELETE:.*']

requireでライブラリを読み込み、useで使用を宣言(?)します。この時、:skipにPOST, PUT, DELETEメソッドの任意のページを指定してRack::Csrf.callメソッド内でのチェックを無効にしてしまいます。

表示の切り替え

set :show_exceptions, false

Sinatraの例外表示処理を無効にします。trueにすればいつものトレース画面が表示されます。

チェック用メソッド

def check_csrf
   raise Rack::Csrf::InvalidCsrfToken unless params[Rack::Csrf.csrf_field] == session['csrf.token']
end

CSRFチェック用のメソッドです。

フィルタ

before do
   check_csrf if ['POST', 'PUT', 'DELETE'].include?(env['REQUEST_METHOD'])
end

beforeフィルタ内にこのように書くことで、POST, PUT, DELETEメソッドの全てのページでCSRFのチェックを行います。これで普通にRack::Csrfを利用した場合と同じ動作となり、かつ例外が発生したときにerrorハンドラーで処理できるようになります。

もし、特定のページだけCSRFチェックしたい場合は、beforeブロック内のcheck_csrfを削除して、

post '/foo' do
  check_csrf
   :
end

のように、チェックしたいページでの処理の先頭にcheck_csrfを持ってきます。

エラーハンドラ

error Rack::Csrf::InvalidCsrfToken do
   "CSRF exception!!"
end

例外を捕捉して処理するためのコードです。

Rack::Csrfの場合は:skipオプションがあったので迂回できましたが、あまりエレガントではないですね。

あまりよく分かってないのですが、Rackの方で、

  • callメソッド内ではraise禁止
  • check_csrfのようなメソッドを準備しておいて、lazyにチェックできるような仕組みを作る
  • callメソッド内で発生した例外をアプリに渡すような仕組みを作る

とかの何らかの枠組みが必要な気がします。