Photo by Mohammad Rahmani on Unsplash
Chrome 从 84 版开始将 Cookie 的 SameSite 属性预设为 Lax,使用到 Third-party cookies 的服务若没有设定 SameSite 都可能受到影响。
概览
Cookies 是网页服务中用来储存状态的机制,常被用在保持登入、购物车、广告追踪等等,但在 Cookies 的广泛使用下,同时也伴随着隐私和安全的疑虑,而 SameSite 的出现就是为了解决这些问题。
First-Party and Third-Party
依据 Cookie 的来源(Set-Cookie)
,每个 Cookie 都有专属的 Domain,以使用者浏览器当下的网址来看,只要 Cookie 的 Domain 和目前的网址相符就是 First-Party,反之就是 Third-Party。
Third-party
例如浏览 a.com
网站时发送 Request 到 third-party.com
并拿到了 third-party.com
的 Cookie,由于浏览器会在 Request 时自动带上相同 Domain 的 Cookie,之后浏览了其他网站如 b.com
时若也发送 Request 到 third-party.com
,Server 就会收到 Cookie,对这两个网站来说 third-party.com
的 Cookie 就是 Third-party。
First-party
如果浏览符合 third-party.com
Domain 的网站也会带上 Cookie,此时这个 Cookie 就称为 First-party。
Same-Origin and Same-Site
刚才的例子提到以 Domain 是否符合来判定 Cookie 的种类,不过更好的说法应该是以 Site 是否相同来判定,而这和常常看到的 Same-origin 是否有关呢?
Origin
Origin 是由 Scheme, Host, Port 组成,判定方式非常简单,只要两个网址的 Scheme、Host 和 Port 都相同就是 Same-origin,其馀皆是 Cross-origin。
Site
Same-Site 的判定则牵涉到 Effetive top-level domains(eTLDs),所有的 eTLDs 被定义在 Public Suffix List 中,而 Site 是由 eTLD 加上一个前缀组成。
举例来说:
github.io
存在 Public Suffix List 之中,加上一个前缀(例如 a.github.io
) 就是一个 Site,因此 a.github.io
和 b.github.io
是两个不同的 Site(Cross-site)。
example.com
不存在 Public Suffix List 之中,但 .com
存在,因此 example.com
是一个 Site,a.example.com
和 b.example.com
就是同一个 Site(Same-site)。
注意 Site 不包含 Port,即使 Port 不同也可以是 Same-site
Why SameSite?
「任何 Request 都带上该 Domain 的 Cookie」的机制同时也带来了安全和其他问题,其中最重要的就是 Cross-site request forgery(CSRF)。
CSRF
假设使用者曾经登入过 example.com
并取得 Cookie,当使用者浏览恶意网站 evil.com
时,网站中的 JavaScript 可以对 example.com/pay?amount=1000
发出 POST Request,浏览器会自动带上 example.com
的 Cookie,使用者就在完全不知情的状况下付了 1000 元,Server 无法判定这个 Request 是从何而来。
限制
Cookie 本身无法被设定为只在 First-party 环境才发送,因此 Request 在任何环境都会带上 Cookie,Server 无法辨识 Request 来源只能照常回复,同时也让 Client 浪费流量送出无用的 Cookie,
解决
有了 SameSite 属性后,就可以个别设定 Cookie 在不同环境下的发送条件。
SameSite
SameSite 属性共有三种值,设定为 Strict
或 Lax
可以限制 Cookie 只在 Same-Site Request 带上,若不填则依据浏览器可能有不同行为,以 Chrome 来说预设值为 Lax
。
Strict
只在 First-party
环境下带上 Cookie,但这有个问题,假设使用者在 example.com
看到一条 FB 贴文连结(假设为 fb.com
),就算使用者曾经登入过 fb.com
取得了 Cookie,点击连结后因为两个网站为 Cross-site,不会带上 Cookie,只能看到登入页面。
因此 Strict
适合用在操作,例如删除贴文、付款等等。
Lax
为了解决 Strict
过于严格的限制,Lax
在以下情况即使是 Cross-site 依然会送出 Cookie:
- 在网址列输入输入网址
- 点击连结
<a href="...">
- 送出表单
<form method="GET">
- 背景转译
<link rel="prerender" href="...">
这几个情况有两个共通点:都是 GET
且皆会触发网页跳转(Navigation)
,如此一来就能避免 Strict
需要重新登入的问题,也不会在浏览其他网站时毫不知情的送出 Cookie。
Lax + POST
然而为了避免破坏某些现有的登入流程,Chrome
目前在 SameSite=Lax
放宽了一点限制,给开发者更多时间喘息。
在 Cookie 被设定的两分钟内,无论 Request Method 是甚麽,只要触发 Top-level 页面跳转都会带上 Cookie,也就是让浏览器换了页面,例如送出表单 <form method="POST">
。
详情请见关于 Lax + POST 的[讨论串](https://groups.google.com/a/chromium.org/g/blink-dev/c/AknSSyQTGYs/m/YKBxPCScCwAJ。
None
想要送出 Third-party cookie
就必须设定为 SameSite=None; Secure
,没错,现在起想要在测试环境送出 Third-party cookie
请准备 https://localhost
。
另外以 XHR/Fetch
送出 Cross-Origin Request 需要另外设定 withCredentials: true
才会带上 Cookie 和让 Response header 的 Set-Cookie
生效,而 Server 端要在 Response header 中设定 Access-Control-Allow-Credentials: true
,JavaScript 才能存取 Response 的内容。
不支援的浏览器
并不是所有浏览器都已经支援最新的 SameSite 规则,因此可以在 Server 加入一些暂时的 Workaround 来支援多种浏览器:
两种 Cookie 都设
此种方式几乎可以解决所有浏览器的问题,缺点就是 Cookie 都会变成两份:
Set-cookie: name=value; SameSite=None; Secure
Set-cookie: name-legacy=value; Secure
Server 端的程式码:
if (req.cookies['name']) {
// 有新的就用新的
cookieVal = req.cookies['name'];
} else if (req.cookies['name-legacy']) {
// 不然就用旧的
cookieVal = req.cookies['name-legacy'];
}
User Agent
以 Request 的 User agent 判断浏览器来决定 Set-Cookie
的内容,这种方式只需要修改设定 Cookie 的程式码,不用修改 Parse 的部分,但这种判断方式相对变数较多,比较容易设定成错误的 Cookie 。
回顾
- 没设 SameSite 属性的 Cookie 都会变成
SameSite=Lax
,Cross-site 环境下无法送出
。 - 想要 Cross-site 送出 Cookie 需要设定
SameSite=None; Secure
。 - 可以用 SameSite sandbox 测试目前用的浏览器是否符合最新的 SameSite 规则。