概要
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
を設定しなくても問題なく動作するアプリケーションであればLax
、Strict
を明示的にセットしてsecureな設定にしておくのが良いと思います。