LoginSignup
2
0

More than 3 years have passed since last update.

GETとPOST(GET以外)で同じコード、でも挙動が違う

Posted at

ちゃんと調査しないと気付かなかったのでメモがてら、誰かが同じ問題に直面したときの助けになれば。

概要

ある日、GETとPOSTで挙動が違うという話が。
コードを見てみたら、コントローラーの同じアクションにGETとPOSTがルーティングされていました。
どう見ても同じ処理。そこに差異は無いので挙動ベースでデバッグしてみることに。
テストコードでは上手く動いていました。でも直接APIを叩いてみると、たしかに違う挙動。

最終的に気付いたのが、問題のあるリクエストではCSRFトークンが渡ってきていないことでした。
アクション内ではsessionが使われており、CSRFトークンが渡されていないことでリクエストは空のsessionに対して処理されていました。これは元々のsessionとは別のものとして扱われ、その結果挙動が変わってしまっていました。

詳細

例としてこのようなコントローラがあるとします。
(POSTの代わりにDELETEにしていますが、挙動は同様です)

app/controllers/session_controller.rb
class SessionsController < ApplicationController
  def destroy
    session.delete(:user_id)
  end
end

そしてこのようにルーティングされています。

config/routes.rb
match 'logout', to: 'sessions#destroy', via: [:get, :delete]

前提として session[:user_id] には既に値が格納されているとします。
このとき、以下の2つはどのように動作するでしょうか?

  • GET /sessions/destroy へのAPIリクエスト
  • DELETE /sessions/destroy へのAPIリクエスト

コードだけ見ると、明らかにどちらも session[:user_id] が削除されそうです。疑いようがありません。ただし、 これはCSRFトークンがリクエストに追加されているか否かで挙動が変わります。

2つともCSRFトークンが追加されていない場合、答えはこうです。

  • GET /sessions/destroy へのAPIリクエストは session[:user_id]削除される
  • DELETE /sessions/destroy へのAPIリクエスト session[:user_id]削除されない

なぜか?
ここで ApplicationController を覗いてみましょう。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end

おっと、なにやら protect_from_forgery というコードが。
※ApplicationControllerに書かれているかは各プロダクトごとに異なります

このprotect_from_forgery with: :null_session が関係しています。
ドキュメントには以下の記述があります。

CSRF protection is turned on with the protect_from_forgery method. By default protect_from_forgery protects your session with :null_session method, which provides an empty session during request.
CSRF保護は、protect_from_forgeryメソッドでオンになります。デフォルトでは、protect_from_forgeryは、リクエスト中に空のセッションを提供する:null_sessionメソッドでセッションを保護します。
(Google翻訳)

この仕様によって、CSRFトークンによる検証が正しくない場合に session の中身が空の状態で、かつ別物として処理されます。また、GETではそもそもCSRFトークンの検証がされないため、session の中身は本来の状態で動作します。
その結果、消したはずの session[:user_id] が消えていないという挙動になります。

振り返り

なぜちゃんと調査しなければ気付かなかったか?

上記の例ではもしかすると気づけたかもしれません。ですが、 protect_from_forgeryApplicationController 側に書かれていたり、それによって普段はあまり意識しなかったりします。これが自分で書いたコードでなければ尚更です。一度経験すればおそらく以後は気付けると思うので、良い経験になりました。

そもそもなぜGETとPOST(GET以外)を同じアクションにルーティングしているのか?

これについては経緯が不明でした。(実際のコードにはコメントがありお気持ちだけは受け止めました)
Railsガイドにもこのような記載があります。

1つのアクションにGETリクエストとPOSTリクエストを両方ルーティングすると、セキュリティに影響する可能性があります。本当に必要な理由がない限り、1つのアクションにすべてのHTTP動詞をルーティングすることは避けてください。

GETとPOSTはRFCで定義された仕様としても別物なので、同じアクションにルーティングするのは避けるべきかなと思います。

さいごに

おそらく今回のものはかなりレアケースかなぁと思います。とはいえどこかの誰かがハマってしまうと悲しいのと、この記事に目を触れた誰かが同様のルーティングをしない、またはルーティングを考える時に注意できる種になればと思います。

お気づきの点があれば気軽にコメントお願いします。些細なことでも歓迎です :tada:

2
0
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
2
0