原文:【Rowan Merewood】SameSite cookies explained
通过学习如何显式标记跨站(cross-site)cookies来保护您的站点。
Cookies 概述
Cookies 是可用于向网站添加持久状态的方法之一。多年来,它们的能力不断发展和壮大,但是给平台遗留了一些问题。为了解决这个问题,浏览器(包括 Chrome,Firefox 和 Edge)正在更改其行为,以强制实施更多保留隐私的默认设置。
每一个 cookie 都是一个 key=value
键值对,以及一些控制何时和何处使用该 cookie 的属性。你可能已经使用过这些属性来设置诸如到期时间之类的信息,或指示 cookie 仅应通过 HTTPS 发送。服务器通过在响应中发送 Set-Cookie
头来设置 cookie。有关所以详细信息,可以深入研究 RFC6265bis。这里只是快速入门。
假设您有一个博客,希望在其中向用户展示“What's new”促销。用户可以撤销该促销,然后就在一段时间内看不到了。您可以将此首选项存储在 cookie 中,将其设置为在一个月(2,600,000 秒)内到期,然后仅通过 HTTPS 发送。该标头看起来像这样:
Set-Cookie: promo_shown=1; Max-Age=2600000; Secure
当您的读者查看满足这些要求的页面时,即他们处于安全连接并且 cookie 的使用期限不到一个月,那么他们的浏览器就会在请求中发送此标头:
Cookie: promo_shown=1
您也可以使用 document.cookie
在 JS 中添加和读取可用于该站点的 cookies。对 document.cookie
进行分配将使用该秘钥创建或覆盖 cookie。例如,您可以在浏览器的 JS 控制台中尝试以下操作:
> document.cookie = "promo_shown=1; Max-Age=2600000; Secure"
< "promo_shown=1; Max-Age=2600000; Secure"
读取 document.cookie
将输出在当前上下文中可访问的所有 cookie,每个 cookie 用分号分隔:
> document.cookie;
< "promo_shown=1; color_theme=peachpuff; sidebar_loc=left"
如果您在一些受欢迎的站点上尝试此操作,您就会发现大多数站点设置的数量远远超过了三个 cookies。在大多数情况下,这些 cookie 是在每个单个请求上发送到该域的,这有很多含义。对于您的用户,上传带宽通常比下载带宽受到更多限制,因此所有出站请求的开销都会增加您第一个字节到达的时间延迟。保守设置您 cookies 的数量和大小。利用 Max-Age
属性可帮助确保 cookie 的停留时间不会超过所需的时间。
什么是 first-party 和 third-party cookies?
如果您回到上一节选择的站点,您可能会注意到针对各种域的 cookie,而不仅仅是您当前正在访问的 cookie。与当前网站的域(即浏览器地址栏中所显示的)所匹配的 cookie 称为 first-party cookies。来自当前站点其他的域的 cookie 称为 third-party cookies。这不是绝对的标签,而是相对于用户的上下文。同一 cookie 既可以是 first-party 又可以是 third-party,这取决于用户当时所在的网站。
继续上面的示例,假设您的一篇博客文章中包含一张特别令人惊叹的猫的图片,并将其托管于 /blog/img/amazing-cat.png
。因为这是一个不错的图片,所以有另一个人直接在他们的网站上使用它。如果访问者访问过您的博客并具有 promo_shown
cookie,那么当他们在其他人的站点上查看 amazing-cat.png
时,将在该图片请求中发送cookie。这对任何人都不太有用,因为promo_shown
并未用于此人网站上的任何内容,它只是增加了请求的开销。
如果这是意想不到的效果,那为什么要这样做呢?因为通过这种机制,站点可以在 third-party 上下文中使用时保持状态。比如,如果您在自己的网站上嵌入 YouTube 视频,则访问者将在播放器中看到“稍后观看”选项。如果您的访问者已经登录 YouTube,则该会话将会通过 third-party cookie 在嵌入式播放器中提供——意味着“稍后观看”按钮将一次性保存视频,而不是提示他们登录或必须从您的页面上离开并返回 YouTube。
Web 的文化属性之一是默认情况下它是开放的。这是让如此多的人在 Web 上创建自己的内容和应用程序的原因之一。然而,这也带来了许多安全和隐私问题。跨站点请求伪造(CSRF)攻击依赖于将 cookie 附加到给定源的请求,而无论谁发起请求。比如,如果您访问evil.example
,则它可以发出对your-blog.example
的请求,并且您的浏览器会附加关联的 cookie。如果您的博客对如何验证这些请求不关心,那么 evil.example
可能会触发删除帖子或添加自己的内容之类的操作。
用户也越来越意识到 cookie 是如何跟踪他们跨多个站点的活动的。但是,到目前为止,还没有一种方法可以明确地表示您对 Cookie 的意图。您的 promo_shown
cookie 仅应在 first-party 上下文中发送,而故意嵌入到其他站点的窗口会话 cookie 则有意在 third-party 上下文中提供登录状态。
使用 SameSite 属性明确声明 Cookie 的使用情况
SameSite
属性(在 RFC6265bis 中定义)使您可以声明 cookie 是否应限于 first-party 或 same-site 上下文。准确了解这里的 “site”(站点) 的含义很有帮助。该站点是域后缀和后缀之前的域的组合。例如:www.web.dev
域是 web-dev
站点的一部分。
关键术语: Same-Site
如果用户在 `www.web.dev`上并从 `static.web.dev` 请求图片,则这是一个 same-site 的请求
公共后缀(public suffix list)对此进行了定义,因此它不仅是 .com
之类的顶级域名,还包括 github.io
之类的服务。your-project.github.ic
和 my-project.github.io
可以算作单独的站点。
关键术语:Cross-Site
如果用户在 `your-project.github.io` 上,并向 `my-project.github.io` 请求图片,则这是 一个 cross-site 请求
在 cookie 上引入 SameSite
属性可提供三种不同的方式来控制此行为。您可以选择不指定属性,也可以使用 Strict
或 Lax
将 cookie 限制为 same-site 请求。
如果将 SameSite
设置为 Strict
,则 cookie 仅会在 first-party 上下文中发送。用用户术语来讲,只有在 Cookie 的站点与浏览器的 URL 栏中当前显示的站点匹配时,才会发送 cookie。因此,如果将 promo_shown
cookie 设置如下:
Set-Cookie: promo_shown=1; SameSite=Strict
当用户在您的网站上时,该 cookie 将与预期的请求一起发送。但是,当您连接到您网站的链接时,例如从另一个网站或通过朋友发送的 email,在该初始请求中,将不会发送 cookie。当您有与功能相关的 cookie 总是处于初始导航之后(例如更改密码或购买商品),但对 promo_shown
来说太严格了。如果您的读者访问该站点的链接,则它们希望发送 cookie,以便可以应用其首选项。
这就是 SameSite=Lax
的应用场景,它允许通过这些顶级导航发送 cookie。让我们从上面重新访问猫的文章的例子,其中另一个站点引用了您的内容。他们直接利用猫的图片,并提供指向您原始文章的链接。
<p>Look at this amazing cat!</p>
<img src="https://blog.example/blog/img/amazing-cat.png" />
<p>Read the <a href="https://blog.example/blog/cat.html">article</a>.</p>
cookie 已经被设置如下:
Set-Cookie: promo_shown=1; SameSite=Lax
当读者在其他人的博客时,如果浏览器请求 amaing-cat.png
,则不会发送该 cookie。但是当读者通过链接访问您博客上的 cat.html
时,该请求将包含 cookie。这使得 Lax
成为影响网站显示的 cookie 的不错选择,而 Strict
对于与您的用户执行的操作相关的 Cookie 很有用。
注意
不论 `Strict` 还是 `Lax` 都不是网站安全的完整解决方案。Cookies 是作为用户请求的一部分发送的,您应该将它们与其他任何用户输入一样对待。这意味着对输入进行消毒和验证。切勿使用 cookie 来存储您认为是服务器端机密的数据。
最后,可以选择不指定值,该值以前是隐式声明您希望在所有上下文中发送 cookie 的方式。在 RFC6265bis 的最新草案中,通过引入 SameSite=None
的新值来明确指出这一点。这意味着您可以使用 None
来明确表示您有意要在第三方上下文中发送 cookie。
如果您使用其他站点使用的服务,例如小部件、嵌入式内容、会员计划、广告或跨多个站点登录,则应使用 `None` 来确保您的意图明确
在没有 SameSite 的情况下更改默认行为
尽管 SameSite
属性得到了广泛支持,但不幸的是,它尚未被开发人员广泛采用。开放的默认发送 Cookie 意味着所有使用用例都可以使用,但使用户容易受到 CSRF 和意外信息泄露的影响。为了鼓励开发人员陈述其意图并未用户提供更安全的体验,IETF 提案《渐进式更好的 Cookies》提供了两个关键更改:
- 没有
SameSite
属性的 Cookies 会被认为是SameSite=Lax
SameSite=None
的 Cookie 也必须指定Secure
,这意味着它们需要安全的上下文
Chrome 自 80 版本其就实现了这些行为。Firefox 从 Firefox 69 其就可以对其进行测试,并将在将来使它们成为默认行为。要在 Firefox 中测试这些行为,请打开 abount:config
并设置 network.cookie.sameSite.laxByDefault
。Edge也计划更改其默认行为。
本文将在其他浏览器宣布支持时进行更新(本译文不一定会跟进哦)
默认 SameSite=Lax
未设置属性:
Set-Cookie: promo_shown=1
如果发送 cookie 时没有设置任何 SameSite
属性
默认行为:
Set-Cookie: promo_shown=1; SameSite=Lax
浏览器会将该 cookie 视为已设置 SameSite=Lax
虽然这是为了应用更安全的默认值,但理想情况下,您应该设置一个明确的 SameSite
属性,而不要依赖浏览器为您应用该属性。这使您对 Cookie 的意图明确,并提高了跨浏览器获得一致体验的机会。
注意
与默认的 SameSite=Lax
相比,Chrome 所应用的默认行为要宽松一些,因为它将允许某些 Cookie 在顶级 POST 请求上发送。您可以在 blink-dev 公告 中看到确切的详细信息。这只是暂时的环节错误,您仍应修复跨站点 Cookies 以使用 SameSite=None; Secure
SameSite=None 必须是安全的
不加 Secure
设置 cookie 会被拒绝:
Set-Cookie: widget_session=abc123; SameSite=None
必须确保将 SameSite=None
与 Secure
属性配对:
Set-Cookie: widget_session=abc123; SameSite=None; Secure
要测试此行为,可以在 Chrome 76 中启用 chrome://flags/#cookies-without-same-site-must-be-secure
,以及在 Firefox 69 中 设置 about:config
: network.cookie.sameSite.noneRequiresSecure
您将需要在设置新的 Cookie 时应用此功能,并主动刷新现有的 Cookie,即使这些 Cookie 的有效期限未到。
如果您依赖于在您的站点上提供第三方内容的任何服务,则还应该向提供商咨询他们正在更新其服务。您可能需要更新依赖项或代码片段,以确保您的网站采用了新的行为。
这两个更改都已正确实现 SameSite
属性的先前版本或根本不支持它的浏览器向后兼容。通过将这些更改应用于 cookies,您可以明确指定其预期用途,而不是依赖浏览器的默认行为。同样,任何尚未识别 SameSite=None
的客户端都应忽略它,并像未设置属性一样继续操作。
警告
很多旧版本的浏览器(包括 Chrome,Safari 和 UC 浏览器)与新的 None
属性不兼容,并且可能会忽略或限制 Cookie。此行为在当前版本中已修复,但是您应该检查流量以确定受影响的用户比例。您可以在 Chromium 网站上查看已知的不兼容客户端列表。
SameSite cookie recipes
有关准确更新 cookie 的详细信息,以成功处理对 SameSite=None
的这些更改以及浏览器行为的差异,请转到后序文章:SameSite cookir recipes