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

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

# 環境
       | バージョン |
    ---+---------+
    OS | macOS 10.14.5
    Ruby | 2.6.3 |
    Sinatra | 2.0.5 |
    Padrino | 0.14.4 |

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

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

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

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

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

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

```ruby
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トークン検証ロジックは以下の通りです。

>```ruby
>      # 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__は今回のような場合に利用できる便利なメソッドを持っています。

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

　__AuthenticityToken#token__で、BASE64エンコードだけでなくマスク処理もしてくれます。これで、__Rack::Session::Pool__と__Rack::Protection::AuthenticityToken__を組み合わせても正しく動作します。

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

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