Note

3年後の自分のために書いています

Rails の API モードで CSRF 対策機能を実装する

半年前に会社の技術ブログの下書きに雑に書いてて公開しどきを忘れたやつ、もったいないのでこちらで公開。


RailsAPI モードはデフォルトでは CSRF 対策機能が有効になってないので、クッキーによるセッション機能を使うと脆弱性を生む可能性がある。

Rails実装済みの CSRF 対策機能を API モードで使う方法を紹介します。

やること

RailsCSRF 対策機能を有効にする

  • ActionController::RequestForgeryProtection をインクルード
  • protect_from_forgery を有効にしたいコントローラに追加

クライアントに CSRF トークンを返す

セッション生成直後に { authenticity_token: form_authenticity_token } をクエリストリング or レスポンスとして渡す(#form_authenticity_token メソッドで rails が生成した CSRF トークンが取れる、ちなみにセッション内の CSRF トークンは session[:_csrf_token] で取れるが、どちらかは暗号化もしくはハッシュ化されてるので値は違うものになる)

クライアント側で POST する際に CSRF トークンを渡す

フォームから POST する場合

<input type="hidden" id="authenticity_token" name="authenticity_token"></input> を仕込んで js で getElementById とかして先ほどクエリストリング or レスポンスで渡された authenticity_token を value に突っ込む

Ajax や fetch の場合

ちなみに rails の ujs を使わずに ajax リクエスト送る時は X-CSRF-Token というヘッダに csrt-token を自前で含めないといけない

ujs では ここ でやってる。

クロスオリジンの検証を無効にする

このままだと request.base_uri と request.origin が違う(クロスオリジンになってる)と InvalidToken のエラーになってしまうので、 config.action_controller.forgery_protection_origin_check = false にする(これは Rails 4 までデフォルト false だったが Rails 5 以降は true になってる)

CSRF を有効にしたセッションのテストをする

ポイントは以下。

  • セッションを作成するエンドポイントを叩く
  • csrf の authenticity_token を params で渡す(これしないと session 取れない)InvalidToken が raise しなかったのが気になる
  • テスト環境の config.cache_store を null_store -> memory_store に(null_store は本当に cache_store を使わないことを意味する)
  • 👆 の cache の後片付けも忘れない

sinatraとrailsとrfcとmdnと徳丸本から学ぶセッションとクッキーと認証とCSRFとXSS

半年前の下書き、めっちゃ途中だけど埋もれるのも嫌なので公開。(あとから追記していく)


元々 csrf についての記事を書こうと思っていたが、csrf について調べるうちセッションとクッキーのことが気になり、rails のセッションとクッキーの実装方法が気になり、フォーム送信と Ajax リクエストって具体的にどう違うんだっけと気になり、Ajax リクエストと fetch って一緒だっけ?違うっけ?と気になり、セッションベースとトークンベースで認証やる場合のメリット・デメリットが気になり、今に至る。

web の技術は全て連関しているのだと思う、これを機に連関する部分を全て理解したい。

セッションとは

ステートレスである HTTP で状態を保持するための仕組み。

クッキーとは

セッションを実現するための仕組み。 サーバ側で Set-cookie して、ブラウザで保持、Cookie はブラウザからサーバ側に常に送信される。

サーバ側ではこの cookie に入った session id もしくは 暗号化された session 情報そのもの(user id や password など?)によってユーザを識別する。

session id のみの場合はなんらかの cache storage(メモリやredisやdynamodb)に session id を key に session 情報を保存しておく。

※ session 情報ってどこまで?passwordは入れてもOK?csrfトークンも入れる?

セッション & クッキー の sinatra での実装

enable :sessions

👆 で session が有効になる。

enable の定義は ここ、中で .set してるだけ。

詳細見てないけど .set されることで デフォルトでは false の sessions が true になるんだと思う。

いろいろあってデフォルトでは session_store として Rack::Session::Cookie が使われてる ことが分かる。

rack の session 周りのコードは https://github.com/rack/rack/tree/master/lib/rack/session にある。

#set_cookie とか定義されてる。ここでヘッダの Set-Cookie に値突っ込んでる。

セッション & クッキーの rails での実装

サーバのどこでキャッシュしてるかとかも書く、キャッシュストレージの違いとか

結論 sinatra と同じく Rack::Session が使われていた。

キャッシュストレージが Memcached 前提だけど以下は参考になる。

Rails の session を完全に理解した

フォーム送信と Ajax リクエストの違い

Ajax リクエストと fetch って一緒だっけ?違うっけ?

社内 esa に書いた

CORS

POST で content-type application/json の場合はプリフライトリクエストが飛ぶ。(その時はサーバ側で header とか method の allow が必要)

See. https://github.com/daido1976/sinatra-vanilla-spa/pull/4

jwt での認証方法

セッションベースとトークンベースで認証やる場合のメリット・デメリット

MPA と SPA の認証方法の違いも書く

jwt トークンはブラウザのどこに保存するのが良いか

そして csrfxss

vagrant ssh したときに裏側で何が起こっているか

結論

以下と同義。

$ ssh vagrant@127.0.0.1 -p 2222 -i ~/vagrant/ubuntu-xx/.vagrant/machines/default/virtualbox/private_key

$ vagrant ssh --debug すると最後の方に INFO ssh: Invoking SSH: ... から続くログが出るのでそれを確認すれば分かる。

補足

vagrant のコードリーディングもちょっとした。

上記のログを出してるのは以下。この lib/vagrant/util/ssh.rbssh 接続の実装の詳細が書かれてるファイルだろう。

https://github.com/hashicorp/vagrant/blob/v2.2.10/lib/vagrant/util/ssh.rb#L225

👆 の Util::SSH.exec が以下で呼ばれてる。

https://github.com/hashicorp/vagrant/blob/v2.2.10/lib/vagrant/action/builtin/ssh_run.rb#L73

👆 の SSHRun クラスを呼んでるのは以下。

https://github.com/hashicorp/vagrant/blob/v2.2.10/plugins/commands/ssh/command.rb#L54-L57

Go + VSCode でサブディレクトリに go.mod を置くと `could not import ... (no package for import ...)` になる

以下のようなやつ。

f:id:daido1976:20200917154706p:plain

環境

原因

gopls("go.useLanguageServer": true で有効)を使いながら、go.mod をサブディレクトリに置いて、プロジェクトのルートディレクトリから VSCode を開いてたのが原因だった。

解決策

gopls を使いたい場合

go.mod の置いてあるディレクトリまで移動してそこで VSCode を開く。

gopls を使わなくてもよい場合

そもそも "go.useLanguageServer": false にして gopls を使わないようにするという手もあった。(その場合は goimports などの command line tools が使われる、むしろ現在はこっちがデフォルト)

gopls は Go modules を正しく使用するプロジェクトのみを想定しているようだ。(公式の README にちゃんと書いてあった…)

https://github.com/golang/vscode-go#language-server

gopls is recommended for projects that use Go modules.

rails stats コマンドで app 以下のディレクリを全て計測できるようにする

$ rails stats コマンドはデフォルトでは以下のディレクトリしか計測しないため、サービスクラスを app/services 以下に置いているような場合、その分は計測されない。

app/controllers
app/helpers
app/jobs
app/models
app/mailers
app/mailboxes
app/channels
app/assets/javascripts
app/javascript
lib/
app/apis
+ test 以下のディレクトリ

See. rails/statistics.rake at 3d428777b05701ca7211a1be723cb1ee0e094bd9 · rails/rails · GitHub

ちなみに上記のようにデフォルトではテストコードの計測は test 以下のディレクトリを対象にしているが、rspec-rails が計測ディレクリ名を test -> specs に上書きしているので、specs 以下のテストコードが計測されるようになっている。

See. rspec-rails/rspec.rake at 13a8a01cc07c93db97b5f1cd57229527441cd626 · rspec/rspec-rails · GitHub

👆 の rspec-rails の上書きを参考にしながら以下のようなコードをプロジェクトの Rakefile に追加してやれば、app 以下のディレクトリ(view 以外)全て計測できるようになる。

# Rakefile
# ...

task stats: "custom:statsetup"

namespace :custom do
  task :statsetup do
    # Push target directories to STATS_DIRECTORIES
    app_dirs = Dir.children('./app').map { |child| "app/#{child}" }
    stats_types = Hash[app_dirs.map { |dir| [dir.split('/').last, dir] }]
    stats_types.each do |type, dir|
      next if type == 'views'
      name = type.capitalize
      ::STATS_DIRECTORIES << [name, dir] unless ::STATS_DIRECTORIES.map(&:first).include?(name)
    end
  end
end

その他

前述のように rspec-rails が計測ディレクリ名を上書きしているため、もし rspec-rails を Gemfile の group :test に置いていると、$ rails stats コマンド実行時にこの上書きがされず、テストコードの計測ができないので group :development, :test などに置いてやる必要がある。

参考

Rails の API モードで静的ファイルをホスティングする

環境

結論

以下、二つの方法がある。

config.public_file_server.enabled を true にして public ディレクトリ以下に置く

静的な HTML ファイルを表示させたいだけなら config.public_file_server.enabled を true に設定した環境で public/index.html を作成すれば、ルーティングやコントローラを実装せずとも表示される。(config.public_file_server.enabled は development 環境などでは デフォルトで true

コントローラで render :file する

以下のどちらかをしないと API モードでは render :file できない。

  • コントローラの継承元を ActionController::API から ActionController::Base に変更する
  • ActionView::Rendering モジュールをインクルードする

説明

Rails ガイドより、

ref. https://railsguides.jp/asset_pipeline.html#アセットパイプラインの使用方法

アセットは引き続きpublicディレクトリ以下に置くことも可能です。config.public_file_server.enabledがtrueに設定されていると、publicディレクトリ以下に置かれているあらゆるアセットはアプリケーションまたはWebサーバーによって静的なファイルとして取り扱われます。

とある通り、config.public_file_server.enabled が true になっている場合は public ディレクトリに置かれているファイルは自動的にホストされる。

development 環境の config.public_file_server.enabled は以下のコマンドなどで確認できる。

$ rails r -e development 'p Rails.application.config.public_file_server.enabled'

参考

Rails の API モードでセッションを有効にする

巷には config/application.rb 内で Rails.app_class.config.api_only = false にすればできる、的な記事が溢れているが、不要な middleware まで読み込みたくない。

環境

差分

デフォルトの CookieStore を使う場合

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CookieStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end

config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware が必要というのが分からずハマった。

👆 しないでも _session_id は保存されたが、session[:user_id] などの情報が保持されなかった。(再度リクエストした時に取得できなかった)

Rails と Rack - Railsガイド

  • ActionDispatch::ContentSecurityPolicy::Middleware

Content-Security-Policyヘッダー設定用のDSLを提供します。

とのこと。これを使うことで _session_id が暗号化(?) されていた。

api_only = false にすると {app_name}_session というキーに暗号化された値が入る(_session_id にも入るけど)

CacheStore を使う場合 ~memory_store の場合~

diff --git a/config/application.rb b/config/application.rb
index 773bbeb..19e3c5b 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -35,5 +35,11 @@ module RailsCsrfProtectionApi
     # Middleware like session, flash, cookies can be added back manually.
     # Skip views, helpers and assets when generating a new resource.
     config.api_only = true
+
+    # Added middleware manually.
+    config.middleware.use ActionDispatch::Cookies
+    config.middleware.use ActionDispatch::Session::CacheStore
+    config.middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
   end
 end
diff --git a/config/environments/development.rb b/config/environments/development.rb
index dc78c00..cbbc46a 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -14,18 +14,11 @@ Rails.application.configure do
   # Show full error reports.
   config.consider_all_requests_local = true

-  # Enable/disable caching. By default caching is disabled.
-  # Run rails dev:cache to toggle caching.
-  if Rails.root.join('tmp', 'caching-dev.txt').exist?
-    config.cache_store = :memory_store
-    config.public_file_server.headers = {
-      'Cache-Control' => "public, max-age=#{2.days.to_i}"
-    }
-  else
-    config.action_controller.perform_caching = false
-
-    config.cache_store = :null_store
-  end
+  # Enable caching.
+  config.cache_store = :memory_store
+  config.public_file_server.headers = {
+    'Cache-Control' => "public, max-age=#{2.hour.to_i}"
+  }

   # Print deprecation notices to the Rails logger.
   config.active_support.deprecation = :log

Rails.app_class.config.cache_store に何も指定しなければ、デフォルトの :file_store になり、 path には "#{root}/tmp/cache/" が使われる。(Rails.app_class.config.cache_store = :file_store と書いた場合は明示的に path の指定が必要になる)

Redis を使いたい場合はここを :redis_cache_store にすれば OK。

api_only の設定による middleware の差分

Rails.app_class.config.api_only が true の場合と false の場合の差分。

$ rails middleware で確認できる。

false の方がたくさん middleware が使われている。

--- tmp/api_true.txt 2020-05-05 05:16:23.000000000 +0900
+++ tmp/api_false.txt 2020-05-05 05:18:06.000000000 +0900
@@ -3,6 +3,7 @@
 use ActionDispatch::Static
 use ActionDispatch::Executor
 use Rack::Runtime
+use Rack::MethodOverride
 use ActionDispatch::RequestId
 use ActionDispatch::RemoteIp
 use Rails::Rack::Logger
@@ -12,7 +13,12 @@
 use ActionDispatch::Reloader
 use ActionDispatch::Callbacks
 use ActiveRecord::Migration::CheckPending
+use ActionDispatch::Cookies
+use ActionDispatch::Session::CookieStore
+use ActionDispatch::Flash
+use ActionDispatch::ContentSecurityPolicy::Middleware
 use Rack::Head
 use Rack::ConditionalGet
 use Rack::ETag
+use Rack::TempfileReaper
 run RailsCsrfProtectionApi::Application.routes

セッションストレージについて

Rails 的には CookieStore 推奨っぽい

あらゆるセッションは、セッション固有のIDをcookieに保存します (注意: RailsでセッションIDをURLで渡すことはセキュリティ上の危険があるため許可されません。セッションIDは必ずcookieで渡さなくてはなりません)。

多くのセッションストアでは、このIDは単にサーバー上のセッションデータ (データベーステーブルなど) を検索するために使われています。ただしCookieStoreは例外的にcookie自身にすべてのセッションデータを保存します(必要であればセッションIDも利用できます)。そしてRailsではCookieStoreがデフォルトで使われ、かつRailsでの推奨セッションストアでもあります。CookieStoreの利点は、非常に軽量であることと、新規Webアプリケーションでセッションを利用するための準備がまったく不要である点です。cookieデータは改竄防止のために暗号署名が与えられています。さらにcookie自身も暗号化されているので、内容を他人に読まれないようになっています。(改ざんされたcookieRailsが拒否します)

以下も参考になりそう。

Rails セキュリティガイド#セッションストレージ

参考