odiak's blog

Sinatra + Vue.js (+ superagent) で CSRF 対策する

最近、 Vue.js でちょっとした web アプリを作ったりしている。

今作っているのは 出費を管理するアプリで、サーバーは Sinatra で、 通信は superagent という npm のパッケージを使っている。
のだが、 CSRF の対策をしていなかったことに気づいたので、実装することにした。

Rails + AngularJS でアプリを作るときにも同じように CSRF の対策をしたので、 Sinatra + superagent でも同じことをやってみた。

Railsのとき参考にしたページ:
angularjs - Rails CSRF Protection + Angular.js: protectfromforgery makes me to log out on POST - Stack Overflow

要は、クッキーに CSRF トークンを入れておいて、リクエストを投げるときにヘッダーでトークンを渡して検証する。

(ちなみに AngularJS はクッキーに入ったトークンをヘッダに入れるのを勝手にしてくれる。ちょっと便利。)

Sinatra に追加するコードはこんな感じ。

helpers do
  # トークンはセッションに保存
  def csrf_token
    session[:csrf_token] ||= SecureRandom.base64(30)
  end

  # GET, HEAD 以外の場合にヘッダを検証する
  def verified_request?
    request.get? || request.head? || env['HTTP_X_CSRF_TOKEN'] == csrf_token
  end

  # クッキーにトークンを設定
  def set_csrf_token_to_cookie
    response.set_cookie('X-CSRF-TOKEN',
      value: csrf_token,
      max_age: 1.week.to_s,
      path: '/'
    )
  end
end


before do
  next unless current_user  # ログインしてない場合は何もしない

  set_csrf_token_to_cookie  # トークンを設定

  # トークンが正しくなければエラーを返す
  unless verified_request?
    status 400
    halt 'Invalid token!'
  end
end

で、クライアント側では、リクエスト時にトークンをヘッダーに含めるように、 superagent の Request#end を書き換える。(cookie をパースするコードは省略)

var request = require('superagent');

var rp = request.Request.prototype
var origEnd = rp.end;
rp.end = function (fn) {
  this.set('X-CSRF-TOKEN', getCookie('X-CSRF-TOKEN'));
  return origEnd.call(this, fn);
};

以上でおしまい。

かなり簡単だったけど、 Sinatra でクッキーを設定するのがうまくできなくて、小一時間悩んでた。 (Sinatra::Cookie の cookie.[]= はダメだった)