LoginSignup
0
2

More than 3 years have passed since last update.

Rack::Session::PoolとRack::Protection::AuthenticityTokenを連携させる方法

Last updated at Posted at 2019-07-12

はじめに

 SinatraやPadrinoでSession管理に用いられるRack::Session::Cookieは、Session Cookieに全セッション情報を保持してしまう為、使いたくありません。代わりにRack::Session::Poolを用いようとしたのですが、CSRFを誤検知するようになってしまいました。
 そもそも、Rack::Session::PoolRack::Protection::AuthenticityTokenに関する情報が少なく、正確でない情報やCSRF対策を無効にするような記事もあったので、調査結果と対策を残しておきます。

環境

バージョン
OS macOS 10.14.5
Ruby 2.6.3
Sinatra 2.0.5
Padrino 0.14.4

現象

 Sinatraの公式ドキュメントでは、Rack::Session::Poolの使い方を以下のように解説しています。

set :sessions, :expire_after => 2592000
set :session_store, Rack::Session::Pool

 しかし、これだけではRack::Protectionが有効にならないとの記述があります。その為、自分で明示的にRack::Protectionミドルウェアを利用する必要があります。

この方法を選択する場合は、セッションベースの保護はデフォルトで有効にならないということに注意することが重要です。

use Rack::Session::Pool, :expire_after => 2592000
use Rack::Protection::RemoteToken
use Rack::Protection::SessionHijacking

 このことから、CSRFを防御したい場合はRack::Protection::AuthenticityTokenを追記すればと考えると思います。

use Rack::Session::Pool, :expire_after => 2592000
use Rack::Protection::AuthenticityToken

 しかし、この設定ではCSRFトークンの受け渡し方法(hiddenタグ等、またはHTTP_X_CSRF_TOKENヘッダ)に関わらずCSRF攻撃を受けたと判定され、「attack reported by Rack::Protection::AuthenticityToken」と報告されてしまいます。

解説

 AuthenticityTokenのCSRFトークン検証ロジックは以下の通りです。

     # Checks the client's masked token to see if it matches the
     # session token.
     def valid_token?(session, token)
       return false if token.nil? || token.empty?

       begin
         token = decode_token(token)
       rescue ArgumentError # encoded_masked_token is invalid Base64
         return false
       end

       # See if it's actually a masked token or not. We should be able
       # to handle any unmasked tokens that we've issued without error.

       if unmasked_token?(token)
         compare_with_real_token token, session

       elsif masked_token?(token)
         token = unmask_token(token)

         compare_with_real_token token, session

      else
         false # Token is malformed
       end
     end

     # ...中略...

     def decode_token(token)
       Base64.strict_decode64(token)
     end

 このdecode_token(token)が問題で、CSRFトークンは必ずBASE64エンコードされている前提で検証されています。Rack::Session::Cookieは、(普通に利用する限り)セッションを書き出す際にBASE64エンコードするのですが、Rack::Session::Poolは何もしません。セッション情報をメモリ上にHashとして持ち続けるだけなのですから、当然といえば当然です。

対策

 Rackコンポーネントを修正しない場合、CSRFトークンをBASE64エンコードするしかありません。幸い、Rack::Protection::AuthenticityTokenは今回のような場合に利用できる便利なメソッドを持っています。

# hiddenタグでCSRFトークンをやり取りする場合の例(erb)
<input type="hidden" name="authenticity_token" value="<%= Rack::Protection::AuthenticityToken.token(session) %>" />

 AuthenticityToken#tokenで、BASE64エンコードだけでなくマスク処理もしてくれます。これで、Rack::Session::PoolRack::Protection::AuthenticityTokenを組み合わせても正しく動作します。

 上述はSinatraを想定していますが、Padrinoも基本的な考え方は変わりません。ただし、PadrinoにはRack::Protection::AuthenticityTokenをPadrino向けに拡張したPadrino::AuthenticityTokenがありますので、そちらを利用するとよいと思います。

# Padrinoの場合(FormHelperを利用)
hidden_field_tag(csrf_param, value: Padrino::AuthenticityToken.token(session))
0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2