LoginSignup
11
6

More than 1 year has passed since last update.

AWS ELBを経由したリクエストでActionController::InvalidAuthenticityTokenが起こったのでデバッグと解決まで

Last updated at Posted at 2020-09-08

背景

AWSで Proxy ELB -> Nginx -> ELB -> Taget Group -> ECS でリクエストを飛ばしてRailsのサービスを動かしたところ、 CSRFトークン対策でエラーになったのでそのデバッグと解決策までの道のり。

CSRFトークン対策でエラーになる

エラー概要

起こっていたエラーはActionController::InvalidAuthenticityToken

CSRFトークン対策とは

https://railsguides.jp/security.html#クロスサイトリクエストフォージェリ-csrf
Railsが標準搭載しているセキュリティ対策です。
セッションに保存されてるtokenとPOST時の authencity_token が一致しているかを検証し、一致していない場合にエラーを吐く。

解決策

nginx.confにproxy_set_header X-Forwarded-SSL on;を追加する。

nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    # これを追加する
    proxy_set_header X-Forwarded-SSL on;
}

エラー検証

tokenが異なっている?

セッションに保存されてるtokenとPOST時の authencity_token が一致しているかを検証し、一致していない場合にエラーを吐く

ならセッションに保存されているtokenとauthencity_tokenが異なっているのか、通常の操作でそうなることがあるのだろうか、ということで実際に検証しているRailsのコードをみてみた。

rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
  (valid_request_origin? && any_authenticity_token_valid?)
end

この verified_request? がfalseの時に InvalidAuthenticityToken のエラーが投げられる。
tokenが異なるということは any_authenticity_token_valid? がfalseということになるので、その予想でデバッグをしてみた。が、any_authenticity_token_valid? はtrueだった。

valid_request_origin? がfalseになっている?

上のコードを見ると、valid_request_origin? がfalseの時にもverified_request?がfalseになる可能性があるので、確認してみた。
すると確かに、valid_request_origin? がfalseだった。

valid_request_origin?の中身を見てみる。

rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
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

valid_request_origin? がfalseになるには request.originrequest.base_url の中身がわかれば理由が分りそうなので出力してみた。
するとrequest.originhttps://〜 なのに対し request.base_urlhttp://〜 となっていた。

つまり上のコードの request.origin == request.base_url の検証部分でfalseになっていることがわかった。

NginxのconfでX-Forwarded-Protoを設定する?

この時点でいろいろ調べると、「NginxからRailsにリクエストが渡される時にHTTPSでNginxにアクセスしてもHTTPとしてRailsに渡されてしまうらしく、これを防ぐために Nginxのconfで X-Forwarded-Protoを使ってRailsにHTTPSであることを知らせる」、という方法がすぐ出てくる。やってみた。

nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # これを追加
    proxy_set_header X-Forwarded-Proto https;
}

けどダメだった。
試しにRailsでリクエストヘッダを出力してみると "X-Forwarded-Proto": "http"となっていた。

どこかでhttpsからhttpに上書きされている?

その通りで、これはELBの性質上でした。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html
image.png

今回、ProxyとなるELBから動かしているRailsのサービスに紐づくELBに対してリクエストが送られてくるが、ここはHTTPで送られてくる。

Application Load Balancer および クラシックロードバランサー は、クライアントに返信する応答のプロキシの後のクライアントの入力リクエストからの接続ヘッダーを優先します

とのことで、NginxからELB間のHTTP通信が優先されてリクエストヘッダの X-Forwarded-ForX-Forwarded-ProtoX-Forwarded-Portが書き換えられてしまっていた。

じゃあどうする

requestオブジェクトはRackで作られているらしいのでそこのコードを見てみた。

rack/lib/rack/request.rb
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

# 省略

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

base_url の作られ方から、 schemehttps になれば良い。
schemahttps になるにはいくつか条件があるけれど 、今回はget_header(HTTP_X_FORWARDED_SSL) == 'on' になるようにすればいけそう!
(HTTP_X_FORWARDED_SSL はELBに書き換えられる心配もない)
ということで、Nginxのリクエストヘッダに X_Forwarded_SSL を追加してみた。

nginx.conf
# もっと本当は書いてあるけど省略
server {
    listen       80;
    server_name  hoge.jp;

    proxy_set_header Host $host;
    # この下2つはELBに書き換えられちゃう
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    # これを追加する
    proxy_set_header X-Forwarded-SSL on;
}

結果

エラーが出なくなった!
デバッグしてみたらちゃんとリクエストヘッダにX_Forwarded_SSLが追加されて schemehttpsになっていた。
image.png

11
6
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
11
6