探索防止 CSRF 的 SameSite cookie 属性

2021-08-07 03:21:42

在阅读 Yan Zhu 关于她在 OkCupid 中发现的 JSON CSRF 漏洞的优秀文章时,一件事让我感到困惑:我的印象是,现在的浏览器默认将 cookie 视为 SameSite=Lax,所以我希望像 Yan 描述的那样进行攻击不适用于现代浏览器。这让我陷入了探索 SameSite 实际工作方式的困境,包括在此过程中构建交互式 SameSite cookie 探索工具。这是我学到的。快速回顾:假设您的应用程序中有一个页面允许用户删除其帐户,网址为 https://www.example.com/delete-my-account。用户必须使用 cookie 登录才能激活该功能。如果您创建该页面来响应 GET 请求,我作为一个邪恶的人可以在 https://www.evil.com/force-you-to-delete-your-account 上创建一个页面来执行以下操作:如果我能得到你访问我的页面,我可以强制你删除你的账号!但是您比这更聪明,并且您知道 GET 请求应该是幂等的。您实现端点以要求 POST 请求。事实证明,如果我可以诱使用户访问带有以下恶意 HTML 的页面,我仍然可以强制删除帐户:

CSRF 是一个极其常见和令人讨厌的漏洞——特别是因为它默认是一个漏洞:如果你不知道 CSRF 是什么,你的应用程序中可能就有它。传统上,解决方案是使用 CSRF 令牌——隐藏的表单字段,它们“证明”用户来自您自己网站上的表单,而不是托管在其他地方的表单。 OWASP 将此称为双重提交 Cookie 模式。 Django 等 Web 框架为您实现 CSRF 保护。我构建了 asgi-csrf 来帮助向 ASGI 应用程序添加 CSRF 令牌保护。显然,如果我们根本不必担心 CSRF 会更好。据我所知,从 2016 年 6 月开始指定 SameSite cookie 属性的工作。这个想法是向 cookie 添加一个额外的属性,指定是否应该将它们包含在从托管在另一个域上的页面向域发出的请求中领域。 SameSite=None——cookie 在“所有上下文”中发送——或多或少是在 SameSite 被发明之前的工作方式。 SameSite=Strict — 仅针对源自同一域的请求发送 cookie。即使从站外链接到达站点也不会看到 cookie,除非您随后刷新页面或在站点内导航。

SameSite=Lax — 如果您通过跟随来自另一个域的链接导航到该站点,则会发送 cookie,但如果您提交表单,则不会发送 cookie。这通常是您想要防止 CSRF 攻击的内容!该属性由服务器在 set-cookie 标头中指定,如下所示: 为什么不习惯性地使用 SameSite=Strict?因为如果有人点击您网站的链接,他们的第一个请求将被视为他们根本没有登录。那很糟!因此,使用 SameSite=Lax 显式设置 cookie 应该足以保护您的应用程序免受 CSRF 漏洞的影响……前提是您的用户拥有支持它的浏览器。 (我可以使用报告 93.95% 的属性的全球支持 - 不足以让我停止习惯性地使用 CSRF 令牌,但我们正在到达那里。)这就是事情变得有趣的地方。如果设置的 cookie 完全没有 SameSite 属性,浏览器应该如何处理?在过去的一年里,所有主要浏览器都在改变它们的默认行为。目标是将缺失的 SameSite 属性视为 SameSite=Lax——默认提供 CSRF 保护。

我发现很难追踪是否以及何时进行了这种更改:Chrome/Chromium 提供了最好的文档——他们声称已在 2020 年 8 月将新的默认值提高到 100% 的用户。Android 中的 WebViews 仍然具有旧的默认行为,计划在 Android 12(尚未发布)中修复。 Firefox 有一个自 2020 年 8 月起的博客条目,上面写着“从 Firefox 79(2020 年 6 月)开始,我们将其推广到 50% 的 Firefox Beta 用户群”——但我找不到任何后续更新。我启动了一个 Twitter 线程来尝试收集更多信息,所以如果你知道发生了什么更详细的情况,请在那里回复。假设以上所有,谜团仍然存在:Yan 的漏洞如何未能被浏览器阻止?在 Twitter 上反复讨论之后,Yan 提出答案可能就是这个细节,隐藏在功能的 Chrome 平台状态页面上:Cookies 默认为 SameSite=Lax。注意:Chrome 将对不到 2 分钟前没有 SameSite 属性的 cookie 设置例外。尽管正常的 SameSite=Lax cookie 要求顶级跨站点请求具有安全(例如 GET)HTTP 方法,但此类 cookie 也将与非幂等(例如 POST)顶级跨站点请求一起发送。未来将取消对这种干预(“Lax + POST”)的支持。

看起来 OkCupid 正在设置他们的身份验证 cookie 没有 SameSite 属性......这让他们受到基于表单的 CSRF 攻击,但仅在设置 cookie 后的 120 秒内!服务器端 Python (Starlette) Web 应用程序,用于设置具有不同 SameSite 属性的 cookie。这是托管在 Vercel 上的 https://samesite-lax-demo.vercel.app/ 不同域上的 HTML 页面,链接到该 cookie 站点,提供针对它的 POST 表单,从中嵌入图像并执行一些获取() 请求反对它。这是在 https://simonw.github.io/samesite-lax-demo/ 托管在两个不同的域上对于该工具显示正在发生的事情至关重要。我选择了 Vercel 和 GitHub Pages,因为它们都可以轻松设置以从 GitHub 存储库持续部署更改。在不同浏览器中使用该工具有助于准确显示跨域 cookie 的情况。 SameSite=Strict 像您期望的那样工作。遵循从静态站点到应用程序的常规 <a href=...> 链接并查看严格 cookie 在到达时如何不可见 — 但在您刷新该页面时变得可见,这一点特别有趣。我在 <img src="/cookies.svg"> 图像标签中包含了一个动态生成的 SVG,它显示了对请求可见的 cookie(使用 SVG <text>)。该图像显示了嵌入 Vercel 域时的所有四种类型的 cookie,但是当嵌入 GitHub pages 域时,它就大不相同了:

如果不包含 Secure 属性,Chrome 将不允许您设置 SameSite=None cookie。我还添加了一些 JavaScript,可以对 /cookies.json 端点进行跨域 fetch(..., {credentials: "include"}) 调用。在我添加服务器端标头 access-control-allow-origin: https://simonw.github.io 和 access-control-allow-credentials: true 之前,这根本没有发送任何 cookie。完成后,我在三个浏览器中得到了与上述 <img 测试相同的结果。 Safari 忽略 SameSite=None 原来是这个错误:SameSite=None 或 SameSite=invalid 的 Cookie 被视为严格 - 它被标记为已修复但我不确定修复是否已经发布 - 它似乎没有被修复在我的 macOS 10.15.6 笔记本电脑或我的 iOS 14.7.1 iPhone 上。最令人兴奋的是,我能够使用该工具复制 Chrome 两分钟窗口错误!每个 cookie 的值都设置为创建时的时间戳,我添加了代码来显示 cookie 是在多少秒前设置的。这是一个动画,展示了表单提交导航上的 Chrome 如何看到在 114 秒前设置为 SameSite 的 cookie,但该 cookie 在 120 秒后不再可见。关于 CSRF,您应该考虑的最后一个注意事项:SameSite=Lax 仍然允许来自主域子域的表单提交携带其 cookie。这意味着,如果您的子域之一存在 XSS 漏洞,则主域的安全性将受到威胁。由于子域托管其他可能有自己安全问题的应用程序是很常见的,因此为 Lax cookie 放弃 CSRF 令牌可能不是一个明智的步骤!

作为应用程序开发人员,您应该使用 SameSite=Lax 设置所有 cookie,除非您有很好的理由不这样做。大多数 Web 框架现在默认执行此操作 - Django 在 2018 年 8 月在 Django 2.1 中提供了对此的支持。您还需要 CSRF 令牌吗?我是这样认为的:我不喜欢启动旧浏览器(可能借用过时的计算机)的用户容易受到这种攻击的想法,我担心上述子域问题。如果您为浏览器供应商工作,请更轻松地找到有关默认行为是什么以及何时发布的信息!