ELB

【Rails/Nginx/ELB 】ActionController::InvalidAuthenticityToken HTTP Origin header (https://example.com) didn’t match request.base_url (http://example.com)の解決法

下のようなインフラ構成でPOSTする際にActionController::InvalidAuthenticityTokenのエラーが出てしまったので、出てしまった理由と解決策をメモしていきたいと思います。ELBまではhttps通信で、そこから先はhttp通信で行っていることを前提とします。

ActionController::InvalidAuthenticityTokenとは

CSRFの対策に関係するエラーです。
まずはCSRF(クロスサイトリクエストフォージェリ)とはというところから書いていきます。

CSRFとは

「重要な処理」に対して利用者の意図したリクエストであることを確認する必要がありますが、これが抜けていると罠サイトを閲覧しただけで、利用者のブラウザから勝手に「重要な処理」を実行させられる場合があります。「重要な処理」の例をあげます。

  • 利用者のアカウントによる物品の購入
  • 利用者のアカウントによるSNSや問い合わせフォームなどへの書き込み
  • 利用者のパスワードやメールアドレスの変更など

CSRFが生まれる原因

CSRFが生まれる背景としては以下のWebの性質があります。

  • form要素のaction属性にはどのドメインのURLでも指定できる。
  • クッキーに保管されたセッションは対象サイトに自動的に送信される。

CSRFの例としてはRailsガイドが分かりやすかったです。

CSRF対策

対策として、正規利用者の意図したリクエストであることを確認することが重要です。具体的には以下の2つが挙げられます。

秘密情報の埋め込み

これはRailsでもやられている手法です。application_html.erbを見ると<%= csrf_meta_tags %>が埋め込まれていて、ブラウザの表示を見るとトークンが生成されていることが分かります。

また、_<アプリ名>_sessionという形でクッキーを送信します。Railsはこれらを比較して、正しいリクエストであることの確認をします。

リクエスト元の確認

CSRFの手法では罠サイトからのアクセスになるので、オリジンヘッダーは罠サイトになります。対象の操作でこれを確認することでCSRFの対策になります。

今回ハマったエラー

ところで今回ハマったエラーはというと、ActionController::InvalidAuthenticityToken HTTP Origin header (https://example.com) didn't match request.base_url (http://example.com)というエラーです。エラー文を読むと、オリジンヘッダーと実際に届いたリクエストのオリジンが正しくないと言われています。なぜそんなことが起こっているのでしょうか。オリジンとはスキーマ、ホスト、ポートからなるものです。インフラ構成ではELBまではHTTPSでそこから先はHTTPで通信しています。つまり、オリジンヘッダーはhttps://example.comに対して、実際に届いたリクエストはhttp://example.comということになります。このため、エラーが起こっていたことが分かります。

具体的なコードでいうとこの部分です。

      def unverified_request_warning_message # :nodoc:
        if valid_request_origin?
          "Can't verify CSRF token authenticity."
        else
          "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
        end
      end

vaild_request_origin?がfalseなら該当のエラーがでそうです

      def valid_request_origin? # :doc:
        if forgery_protection_origin_check
          # We accept blank origin headers because some user agents don't send it.
          raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
          request.origin.nil? || request.origin == request.base_url
        else
          true
        end
      end

request.origin == request.base_urlがfalseならこのメソッドからfalseが返りそうです。request.base_urlはRackのメソッドです。

      def base_url
        "#{scheme}://#{host_with_port}"
      end

schemaメソッドを見てみます。

      def scheme
        if get_header(HTTPS) == 'on'
          'https'
        elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
          'https'
        elsif forwarded_scheme
          forwarded_scheme
        else
          get_header(RACK_URL_SCHEME)
        end
      end

ようやく現れました。
HTTP_X_FORWARDED_SSLヘッダーがonならhttpsになるようです。NginxへはhttpでくるのでそこでユーザからはSSL通信ですよということを明示的に設定する必要があるようです。

  location @unicorn {
    # 略
    proxy_set_header X-Forwarded-SSL on; #追記
  }

これは少し余談ですが、ELBを使っているとヘッダーを勝手に書き換えてしまうようです。なので、Nginx側で設定が必要になるというわけですね。

https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html

これでエラーが消えました👏

参考

https://qiita.com/ruru8/items/345bec2f8aff64cd0962

ABOUT ME
sakai
三重出身の28歳。前職はメーカーで働いていて、プログラミングスクールに通って未経験からWeb業界に転職しました。Railsをメインで使っていて、AWSも少しできます。音楽を聞くこととYoutubeを見るのが好きです。最近はへきトラ劇場にハマってます

COMMENT

メールアドレスが公開されることはありません。