LoginSignup
27
14

More than 3 years have passed since last update.

RailsでのCookieのSameSite, Secureの対応

Posted at

概要

2020年2月にChromeのバージョンが80にアップデートされました。
これはCSRFを防ぐためChromeのCookieのSameSite属性をデフォルトでSameSite=Laxにしようというものです。アップデート前はSameSite=Noneと同じ挙動をしていました。

つまりアップデート前までSameSite=Noneでないと正常に動作しないアプリケーションについては明示的にNoneを指定する必要があります。

https://developers-jp.googleblog.com/2019/11/cookie-samesitenone-secure.html
から抜粋

2月のChrome 80 以降、SameSite 値が宣言されていない Cookie は SameSite=Lax として扱われます。
外部アクセスは、SameSite=None; Secure 設定のある Cookie のみ可能になります。
ただし、これらが安全な接続からアクセスされることが条件です。

SameSiteの挙動の説明

SameSite 説明
None クロスドメインでCookieの受け渡しが可能
(ただしSecure=Trueの設定は必須のためhttpの環境だと正常に動作しないはずです)
Lax クロスドメインでGETメソッドであればCookieの受け渡しが可能だがPOSTメソッドは不可
Strict クロスドメインでGET、POSTメソッド両方ともCookieの受け渡しは不可

ただし、これはChromeでの挙動になります。

それ以外のブラウザでは違った挙動をするものがあるので注意が必要です。詳しくは下記のリンク先をご覧ください。
https://www.chromium.org/updates/same-site/incompatible-clients

この中で結構問題があると思われるのは
Versions of Safari and embedded browsers on MacOS 10.14 and all browsers on iOS 12. These versions will erroneously treat cookies marked with SameSite=None as if they were marked SameSite=Strict. This bug has been fixed on newer versions of iOS and MacOS
です。
現時点(2020/2/27)でiOS 12、MacOS 10.14はそれほど古いバージョンではなく使用しているユーザーも多いと思われます。このユーザーに対してはSameSite=Noneと設定しているとSameSite=Strictと同じ挙動をするそうです。
対応するとしたらブラウザによってcookieを書き換える必要がありそうです。

Ruby on RailsでSameSite=Noneを設定する対応例

こちらの記事ではgemでの対応が記載されているのでgemで対応したい方はこちらが良いと思います。
(rails_same_site_cookie gemで、RailsアプリにChrome 80向けのSameSite属性を指定する)

以降はRuby on Railsでの実装例を記載します。

最初にrack gemのバージョンを確認します。
rackが2.0系だとSameSite=Noneの対応が入っておらずエラーになるのでrackのバージョンアップをおこなうかRack::Utilsのモンキーパッチ以下のように作成します。
rackのバージョンアップ時に削除漏れがあるといけないので内容にTODOを記載しておきましょう。

# -*- encoding: binary -*-
# TODO: rack2.1.0以降だとsamesite=Noneの設定が入っているのでソースファイルごと削除する
module Rack
  # Rack::Utils contains a grab-bag of useful methods for writing web
  # applications adopted from all kinds of Ruby libraries.

  module Utils
    def add_cookie_to_header(header, key, value)
      case value
      when Hash
        domain  = "; domain=#{value[:domain]}"   if value[:domain]
        path    = "; path=#{value[:path]}"       if value[:path]
        max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
        # There is an RFC mess in the area of date formatting for Cookies. Not
        # only are there contradicting RFCs and examples within RFC text, but
        # there are also numerous conflicting names of fields and partially
        # cross-applicable specifications.
        #
        # These are best described in RFC 2616 3.3.1. This RFC text also
        # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
        # fixed length format with space-date delimited fields.
        #
        # See also RFC 1123 section 5.2.14.
        #
        # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
        # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
        # the space delimited format. These formats are compliant with RFC 2822.
        #
        # For reference, all involved RFCs are:
        # RFC 822
        # RFC 1123
        # RFC 2109
        # RFC 2616
        # RFC 2822
        # RFC 2965
        # RFC 6265
        expires = "; expires=" +
          rfc2822(value[:expires].clone.gmtime) if value[:expires]
        secure = "; secure"  if value[:secure]
        httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
        same_site =
          case value[:same_site]
          when false, nil
            nil
          when :none, 'None', :None
            '; SameSite=None'.freeze
          when :lax, 'Lax', :Lax
            '; SameSite=Lax'.freeze
          when true, :strict, 'Strict', :Strict
            '; SameSite=Strict'.freeze
          else
            raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
          end
        value = value[:value]
      end
      value = [value] unless Array === value

      cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
        "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"

      case header
      when nil, ''
        cookie
      when String
        [header, cookie].join("\n")
      when Array
        (header + [cookie]).join("\n")
      else
        raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
      end
    end
    module_function :add_cookie_to_header
  end
end

修正箇所は↓の部分を追記しただけです。

when :none, 'None', :None
            '; SameSite=None'.freeze

rackが2.1系以降であれば対応されているので上記の対応は不要です。

使い方はこんな感じでいけると思います。

cookies[:hogehoge] = { value: "sample value", expires: 1.hour.from_now, same_site: "None", secure: true }

環境によってSecure=True or Falseを設定したい場合

以下のようなClassを作成します。

# frozen_string_literal: true

class SecureCookieWithSameSiteLax

  def self.secure?
    Rails.env.staging? || Rails.env.production?
  end

end

使い方はこんな感じです。

secure = SecureCookieWithSameSiteLax.secure?

cookies[:hogehoge] = { value: "sample value", expires: 1.hour.from_now, same_site: "Lax", secure: secure }

production環境、staging環境ではSameSite=Lax Secure=True
development環境ではSameSite=Lax Secure=False
としたい場合の実装方法となります。

SameSite=Noneを設定しなくても問題なく動作するアプリケーションであればLaxStrictを明示的にセットしてsecureな設定にしておくのが良いと思います。

27
14
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
27
14