導入
人生で初めてAPI設計におけるセキュリティについて考えましたので、最終的な実装内容にも簡単に触れながら考えを整理します。
各種環境について
サーバー・クライアント関係はAPI側がRails、クライアント側がReact(axiosによる送信)となる。
Rails側のエンドポイント設計はgrapeというgemによるもの。
サーバ: localhost:3000
クライアント: localhost:3001
マシンはMacBook Pro (13インチ, M1, 2020)を使用。
% ruby -v
ruby 3.2.1
% rails -v
Rails 7.0.4.2
grapeのバージョンは1.7.0。
React側は react@18.2.0
、 axios@1.3.4
。
実装手順と説明
protect_from_forgeryはデフォルトのまま
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
...
rails newの時から触っていません。
これを「何かしらエラーが起きてしまう」からと protect_from_forgery with: :null_session
にしてしまうと、アプリケーション全体でcsrf-tokenがなくともリクエストを通すようになります(で、認識が合っているかと…)。
検証そのものを行わなくするには config.action_controller.allow_forgery_protection = false
というように設定するそうです。
forgery_protection_origin_checkの設定
module App
class Application < Rails::Application
# ~ 中略 ~
config.action_controller.forgery_protection_origin_check = false
end
end
CSRFの追加対策として、HTTPのOriginヘッダーがサイトのoriginと一致することをチェックすべきかどうかを設定します。
今回はrack-corsでオリジン関係の設定を行うため、Rails側の設定を false
にします。
rack-corsの導入
gem 'rack-cors'
インストールを行い、設定を書きます。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:3001'
resource '*',
headers: :any,
methods: [:get, :post],
expose: ['X-CSRF-Token']
end
end
今回はトークンをヘッダーに仕込みます。
トークンの受け渡しについて
サーバ上のユーザ認証、トークン生成時
クライアント側の情報を元にユーザ認証が成功したとします。
grapeのドキュメントにもある方法で、レスポンスヘッダーにトークンを乗せます。
post :endpoint_name do
# 処理が成功
header 'X-CSRF-Token', token
# body内容の記述
end
クライアント側によるトークンの受け取り
参考サイトを参考に、トークンをaxios上の設定として保持します。
.then(response => {
axios.defaults.headers.common['X-CSRF-Token'] = response.headers['x-csrf-token']
以後、リクエスト発行時にヘッダーを毎回設定します。
サーバ上のリクエスト処理
リクエストを受け取る際に、Rails上での参照は下記で成功しました。
env['X-CSRF-Token']
ちなみに今回はトークン参照後、サーバ側の自作メソッドでトークンを検証するようにしています。
セキュリティ上の要件を検証
セルフチェック
こちらの記事で、CORS設定を行う上でのCSRF対策要件として掲げている、
-
重要な操作を行う場合は X-CSRFToken のようなヘッダを付けてリクエスト送信を行うような仕様にする
-
このヘッダのチェックはサーバー側でも正しく行う
-
サーバーでは正しく Access-Control-Allow-Origin ヘッダを出力する
はオールクリアしているはずかと思います。
暗号化に対する認識
手元に有用な参考文献がなくて恥ずかしいのですが、HTTPヘッダーはTLS通信時に暗号化される、という認識で合っていると思っています。
ですので、現在はブラウザ上にて簡単にトークンを確認できてしまいますが、追々HTTPSへ対応する際にこの問題は解消されるはずです。
追記: 暗号化されないそうです…。あくまでもHTTPSだと暗号化された通信路が開設されるというだけですね。
セキュリティ面における本実装の不安
より安全なセキュリティ要件に基づいて設計を行う場合、諸々についてはcookieによる受け渡しで行うのが良いかと思います。
というのも、今回行っているaxiosの設定情報の活用について、それがどれだけ安全なものなのかが分からなかったです。もし今の状態では、攻撃者がJavascriptによってトークンを簡単に奪えてしまうとしたら、やはりHTTPSを前提にcookieによる受け渡しによって設計を行うと安全そうです。
メモ代わりに補足しますが、cookieを用いた設計へと変更する場合、
- HTTPS対応以降、cookieのSameSite属性をNone、Secure属性にチェックを入れる
- CORS設定上で
credentials
をtrueにする - Rails上にてレスポンス内容に
cookie[:token]
と書いて、set-cookieを行う
となるかと思われます。
トークンの保存場所としてcookieを使うべきか否か、という議論は今回は不問とさせてください。
参考サイト
実装はこちらのコードをおおいに参考しました。