Edited at

Rails API mode+deviseなSPAでtokenによるCSRF対策をする

こんにちは、 @mshibuya です。

RailsをバックエンドとしたSPAを開発するにあたり、tokenベースのCSRF保護をどう作り込むかの知見があまり見つからなかったので自分で作っちゃいました、という記録です。。

なおこの記事はオプトテクノロジーズアドベントカレンダー17日目のエントリーです。


動機

通常のWebアプリケーションと同様、SPAにおいてもCSRF脆弱性は大いに問題になるわけですが、とはいえ対策方法についてまだ「これで決まり!」というレベルの定番の手法がないように感じています。

「SPA CSRF」で検索して上位に出てくる弊社エンジニア @uryyyyyyy によるSPAでのセッション管理とセキュリティのエントリにおいても、CSRF Tokenを利用する方法について


しかしSPAでは、フロントのコードがサーバーを経由せずに送られてきたり、Formが後から動的に作られうるので、仕込むタイミングが少し難しいかもしれません。


とだけ触れており、具体的な手法への言及はありません。

とはいえ、OWASPによるCross-Site Request Forgery (CSRF) Prevention Cheat Sheetにおいては


We recommend token based CSRF defense (either stateful/stateless) as a primary defense to mitigate CSRF in your applications.


とされており、tokenベースの対策が第一選択となるのは確かです。

そこで、SPAにおいてできるだけ透過的な(=保護漏れのリスクを最小限にできる)CSRF保護をRails APIをバックエンドとして実装してみます。


検討したこと


Json Web Token(JWT)等の利用

JWT(JSON Web Token)でCSRF脆弱性を回避できるワケを調べてみた話

上記エントリにあるよう、CSRF対策みたいなことを考えずに済むのは魅力的な話ではあったのですが、とはいえ

JSON Web Token(JWT)って結局使っていいの?

のような話から、セキュリティに係る技術としてはまだ十分に成熟していないと判断しました。


Originチェックのみで誤魔化す

これは、上述OWASP資料において明確に


These defense-in-depth mitigation techniques are not recommended to be used by themselves (without token based mitigation) for mitigating CSRF in your applications.


と挙げられている中にOriginチェック等が該当しているため、この対策のみ行うことをCSRF対策として十分とは判断しませんでした。


実現方法

OWASP資料中の同期トークンパターンを採用しました。サンプルリポジトリを公開してありますのでご覧ください。

https://github.com/mshibuya/rails-api-csrf-protection

要点についてはこのコミットを見るのが手っ取り早いと思います。コンセプトとしては単純で、


  • レスポンスに X-CSRF-Token ヘッダを乗せて返す処理をafter_actionで引っ掛けておく

  • フロント側ではレスポンスにある X-CSRF-Token ヘッダを毎回チェックし、ローカルストレージに保存する

  • フロント側からPOSTする際には X-CSRF-Token ヘッダにローカルストレージから取り出したtokenをセットしてリクエスト

  • ログイン時は送るべきtokenがないのでCSRFチェックをスキップ


    • 厳密にはこれはまずいんですが、インパクトが大きくないのは確かなので宿題ということで…



という感じです。特に複雑なところはないですね。。

フロントエンドは雑にvue.jsで作っています。ページ内にログインボタンとかtokenクリアボタンとかついてるので、tokenなしでリクエストしたらどうなるかとか確かめられるはずです(実装が適当なのは急ごしらえなので勘弁を…)。


まとめ

Rails API modeをバックエンドとしたSPAにおいて同期トークンパターンによりCSRF保護する方法を紹介しました。

なにか不備やツッコミ等ありましたらお寄せいただければと…。わりとちゃんとしている(と思っている)わりには手軽にできるので、皆様もお試しくださいー