CSRF 漏洞的末日?关于 Cookie SameSite 那些你不得不知道的事
眼下,新冠病毒在全球各处肆虐,为了阻断病毒的传播,各国纷纷要求民众居家隔离。与此同时,为了提升 web 的安全性,Chrome 80 也开始要求 cookie 进行站点隔离。
我们都知道,服务端在设置 cookie 的时候,除了 cookie 的键和值以外,还可以同时给 cookie 设置一些属性,例如:
Expires
Max-Age
Domain
Path
Secure
HttpOnly
SameSite
开发者通过这些属性来告知浏览器在什么时候,什么情况下使用该 cookie。其中的 SameSite 属性就是本文要讨论的主角,后面的内容主要包含以下三点:
SameSite 属性基础
SameSite 属性的演进
SameSite 属性带来的影响
SameSite 属性基础
site 的含义
首先要知道:有效顶级域名(eTLD, effective top-level domain)对应的是由 Mozilla 维护的公共后缀列表(Public Suffix List:https://publicsuffix.org/)里包含的域名。
这个列表由两部分组成:
一部分是由域名注册机构提供的顶级域名(例如:.com,.net 等)和部分二级域名(例如:.gov.uk,.org.uk 等)
另一部分是由个人或机构提供的私有域名,例如:github.io,compute.amazonaws.com 等
而 SameSite 里的 site 指的是 eTLD+1,即:有效顶级域名再加上它的下一级域名。
举例说明:
qzone.qq.com 对应的 site 是 qq.com
它的 eTLD 是 .com,eTLD+1 就是 qq.com
vip.qzone.qq.com 对应的 site 也是 qq.com
它的 eTLD 是 .com,eTLD+1 也是 qq.com
bootstrap.github.io 对应的 site 是 bootstrap.github.io 而不是 github.io
它的 eTLD 是 github.io,eTLD+1 是 bootstrap.github.io
同站 (same-site) 请求 VS 跨站 (cross-site) 请求
例如:
-
当 www.baidu.com 的网页,请求 static.baidu.com 域下的图片,这个请求属于同站请求
当 a.github.io 的网页,请求 b.github.io 域下的图片,这个请求属于跨站请求
这里要注意和同源策略里的 same origin 做一下区分。同源指的是同协议、同域名、同端口。同站只看 site 是否一致,不管协议和端口。所以同源一定同站,同站不一定同源。
None
Lax
Strict
SameSite 属性可以用在 HTTP 响应头里:
Set-Cookie: sessionId=F123ABCA; SameSite=Strict; secure; httponly;
也可以在 JS 代码里使用:
document.cookie= "sessionId=F123ABCA; SameSite=Strict; secure;"
使用的时候,SameSite 关键字和它的三个值都不区分大小写。
对于 SameSite=Strict 的 cookie:只有同站请求会携带此类 cookie。
对于 SameSite=None 的 cookie:同站请求和跨站请求都会携带此类 cookie。
Lax 的行为介于 None 和 Strict 之间。对于 SameSite=Lax 的 cookie,除了同站请求会携带此类 cookie 之外,特定情况的跨站请求也会携带此类 cookie。
特定情况的跨站请求指的是:safe cross-site top-level navigations(后文简称:安全的跨站顶级跳转),例如:
点击超链接 <a> 产生的请求
以 GET 方法提交表单产生的请求
JS 修改 location 对象产生的跳转请求
JS 调用 window.open() 等方式产生的跳转请求
反过来,哪些跨站顶级跳转是不安全的呢?例如:
以 POST 方法提交表单产生的请求
通过不同方式发起跨站请求,cookie 发送情况可以简单总结为下表:
最后一行的 prerender 请求有些特殊,它也会携带 SameSite=Lax 的 cookie,相关讨论:specify prerender processing model(https://github.com/w3c/resource-hints/issues/63)
SameSite 的演进
SameSite 的出现
在 cookie 最初的规范 RFC 6265 (https://tools.ietf.org/html/rfc6265) 里是没有 SameSite 属性的。
直到2016 年,https://tools.ietf.org/html/draft-west-first-party-cookies-05 SameSite 属性才被提出。
Cookie 的改进
cookie 最初的行为是:无论是同站请求还是跨站请求都会带上各自域下的 cookie,效果等同于 SameSite=None。
这样的行为导致了一些安全和隐私上的问题:
CSRF 漏洞
跨域信息泄露
为了解决这些问题,出了一个新提案:Incrementally Better Cookies(后文简称 IBC,https://tools.ietf.org/html/draft-west-cookie-incrementalism-00),里面提出了两点改进:
没有声明 SameSite 属性的cookie 被处理为 SameSite=Lax。换句话说:cookie 的默认行为由 SameSite=None 改为 SameSite=Lax
设置为 SameSite=None 的 cookie,必须同时被标记为 Secure。换句话说:只能在 HTTPS 的情况下使用 SameSite=None
浏览器的实现
众所周知,不同浏览器对规范的实现常常不一致。
先来看主流浏览器对 SameSite 属性的支持情况:
Chrome 从 v51 开始支持 SameSite 属性
Firefox 从 v60 开始支持 SameSite 属性
Edge 从 v16 开始支持 SameSite 属性(https://blogs.windows.com/msedgedev/2018/05/17/samesite-cookies-microsoft-edge-internet-explorer/)
Opera 从 v39 开始支持 SameSite 属性
Safari 从 v12 开始部分支持(macOS 10.14 Mojave 之前的版本不支持,macOS 10.15 Catalina 之前的版本还有 bug,https://bugs.webkit.org/show_bug.cgi?id=198181)
具体的支持情况可以参考:https://caniuse.com/#feat=same-site-cookie-attribute
主流浏览器对 SameSite 默认值为 Lax 的实现:
Chrome 从 v76 开始支持手动开启(https://www.chromestatus.com/feature/5088147346030592),从 v80 开始,分批次对用户开启 IBC
Firefox 从 v69 开始支持手动开启(https://groups.google.com/forum/#!msg/mozilla.dev.platform/nx2uP0CzA9k/BNVPWDHsAQAJ)
Edge 计划从 v80 开始测试该特性(https://groups.google.com/a/chromium.org/forum/#!topic/blink-dev/AknSSyQTGYs%5B51-75%5D)
Safari 和 Opera 目前还不支持
具体的支持情况可以参考:https://caniuse.com/#feat=mdn-http_headers_set-cookie_samesite_lax_default
后面的动手实践部分,会分别用 Chrome、Firefox 和 Edge 对 IBC 进行测试。
Chrome 浏览器对 SameSite 属性进行变更的时间线可以查看:https://www.chromium.org/updates/same-site
这里有个小插曲:为确保新冠病毒期间网站的稳定性,尤其是提供基本服务的网站(银行、政府服务、医疗保健等)的稳定性,chromium Blog (https://blog.chromium.org/2020/04/temporarily-rolling-back-samesite.html)宣布暂时回滚 Chrome 80 对 SameSite Cookie 的修改。
浏览器实现中的特例:Lax + POST
前面提到过:安全的跨站顶级跳转请求会携带 SameSite=Lax 的 cookie,例如:以 GET 方法提交表单。按照规范,以 POST 方法提交表单不应该携带 SameSite=Lax 的 cookie。
但是,考虑到很多网站使用了第三方提供的统一登录,在这过程中可能会用到 POST 提交表单,为了不破坏这些网站的功能,Chrome 提出了一个临时解决办法:对于那些没有声明 SameSite 属性的 cookie,如果它的创建时间在两分钟以内,当以 POST 方法提交表单发起请求时,也会携带这类 cookie。同时,控制台会给出警告信息:
需要注意:这只是一个临时解决办法,以后 Chrome 会将其移除,所以开发者不要依赖这一特性。
动手实践
如果你想查看自己的浏览器是否已经完整启用 IBC,可以访问:https://samesite-sandbox.glitch.me/ 进行测试。
测试页面的运行逻辑如下:
访问主页面 https://samesite-sandbox.glitch.me/ ,响应头里设置了 6 个 cookie
主页面中包含一个iframe,指向一个跨域页面:https://googlechromelabs.github.io/samesite-examples/crosssite-embed.html
该跨域页面向主页面所在域名samesite-sandbox.glitch.me 发起一个 Ajax 请求,同时指定 withCredentials=true
这个Ajax 在 iframe 里属于跨站请求,会携带部分 cookie,后端返回的响应就是请求中携带的 cookie 列表
请求如下所示:
响应如下所示:
iframe 接收到 Ajax 响应后,通过postMessage 把结果传递给主页面(用来告知主页面:跨站请求携带了哪些 cookie)
window.parent.postMessage(jsonResponse,"https://samesite-sandbox.glitch.me");
主页面通过比对 cookie 的设置情况和跨站请求 cookie 的发送情况,给出兼容性表格。
Chrome 测试
我这里的 Chrome 版本是 81.0.4044.113(写作本文时 Chrome 的最新版),默认没有启用 IBC。
访问测试页面结果如下:
解释一下上面的表格:
先看 Cookie set 一栏。ck02 带有 ❌ 表示它不符合 IBC,因为它在设置 SameSite=None 的同时没有设置 Secure,按照 IBC 它应该是 not Set
再看 Cross-site 一栏。ck00、ck01、ck02、ck03 值都是 set,表示跨站请求携带了这四个 cookie,但是按照 IBC,跨站请求只应该携带 SameSite=None 且Secure 的 ck01,所以其他三个都带有 ❌
最后看 IBC compliant 一栏。ck00 和 ck03 都是 Careful 表示它俩都在 IBC 规范影响范围内。ck02 值是 Invalid,表示它会被实现了 IBC 规范的浏览器拒绝
此时,如果你切换到控制台,会看到两条警告:
第一个警告是由 ck00 导致:set without the SameSite attribute.
第二个警告是由 ck02 导致:set with SameSite=None but without Secure.
而且从上图可以看到,用 document.cookie 可以获取到所有 cookie。
接下来,在 Chrome 里手动开启 IBC:
访问 chrome://flags
把 #same-site-by-default-cookies 和 #cookies-without-same-site-must-be-secure 改为 Enabled
启用之后,重启浏览器,再次访问测试站点,先删除所有 cookie(这一步很重要),刷新页面,效果如下:
上图中 ck02 由于设置 SameSite=None 的时候,没有设置 Secure,导致它被 Block,Chrome 会把这类 cookie 的背景设置为黄色。同时,在控制台也会输出对应的警告信息。
注意看上图:document.cookie 获取不到 ck02。
切换到网络面板,观察 iframe 里 Ajax 请求的 cookie 发送情况:
Ajax 请求只携带了 SameSite=None 且 Secure 的 ck01,符合 IBC 规范。
Firefox 测试
我这里的 Firefox 版本是 75.0(写作本文时 Firefox 的最新版),默认没有启用 IBC。
访问测试页面,结果如下:
和前面未启用 IBC 的 Chrome 结果一致。
接下来,在 Firefox 里手动开启 IBC:
访问 about:config
把 network.cookie.sameSite.laxByDefault 和 network.cookie.sameSite.noneRequiresSecure 改为 true
清空 cookie 后,再次访问测试页面,结果如下:
和前面启用 IBC 的 Chrome 结果并不一致。区别是 Cross-site 一栏,ck00 和 ck03 的值都是 set,说明:跨站请求携带的 cookie 除了本该携带的 ck01 外,还多带了 ck00 和 ck03。
从上图可以看到:Firefox 已经把 ck00 和 ck03 两个 cookie 的 SameSite 值标记为 Lax,按照规范,跨站 Ajax 请求是不应该携带 Lax cookie,所以这里应该是 Firefox 实现上的一个 BUG。
我们去网络面板印证一下,确实多带了两个 cookie。
Edge 测试
SameSite 带来的影响
在所有 cookie 里,攻击者更关注的其实是用来维持用户登录状态的 session cookie。如果攻击者发起的请求没有携带对应用户的 session cookie,那么网站会将其判定为未登录状态,这就导致那些需要登录才能访问的数据会获取不到,需要登录才能执行的操作会无法进行。
我在本地准备两个测试站点:
www.foo.com:3000 模拟存在漏洞的网站(方便起见,后文描述时省略端口号)
www.evil.com:4000 模拟攻击者的网站
用户在 www.foo.com 登录之后,会得到一个没有设置 SameSite 属性 session cookie,如下图所示:
不设置 SameSite 属性为的是让 cookie 使用 SameSite 默认值,而且,这也和当前大多数网站的行为一致,后文的讨论也是基于这种设定。
CSRF
CSRF 攻击依赖的就是跨站请求会自动带上用户 cookie,进而可以伪造请求,代替用户执行敏感操作。
测试站点 www.foo.com 有一个修改邮箱的接口,存在 CSRF 漏洞。
PoC 如下:
启用 IBC 后,Burp 拦截到的请求如下图所示:可以看到伪造的请求没有携带 cookie,后端响应提示需要登录。CSRF 利用失败。
把上面 PoC 里的 method 改为 get 再次测试,Burp 拦截到的请求如下图所示:伪造的请求携带了 cookie(因为 GET 形式提交表单属于安全的跨站顶级跳转),邮箱成功被修改。
可以看到,IBC 对 POST 形式的 CSRF 漏洞可以起到很好的防御效果。
对于前面提到的浏览器实现中的特例:Lax + POST,也有研究人员总结了一些利用方式(https://medium.com/@renwa/bypass-samesite-cookies-default-to-lax-and-get-csrf-343ba09b9f2b)。
CSWSH(Cross-Site WebSocket Hijacking)
Cross-Site WebSocket Hijacking 利用的是 WebSocket 不受同源策略限制,当用户访问恶意网页时,恶意网页可以向目标网站发起一个 WebSocket 连接,第一个握手请求就是正常 HTTP(S) 请求,会带上用户的认证信息(session cookie 等),如果开发者没有检测握手请求的 Origin,而是仅仅通过 session cookie 对用户进行认证并允许建立 WebSocket 连接,那么攻击者就可以进行 Cross-Site WebSocket Hijacking。更详细的原理可以参考:Cross-Site WebSocket Hijacking(https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html)。
启用 IBC 之后,session cookie 的 SameSite 属性默认值变为 Lax,跨站握手请求不携带 session cookie,也就无法像上面一样成功建立 WebSocket 连接,导致 Cross-Site WebSocket Hijacking 失败。
XSSI(Cross-Site Script Inclusion)
Cross-Site Script Inclusion 是一种允许攻击者跨域窃取特定类型数据的攻击技术。
回想一下,我们经常能看到某个站点的 HTML 页面里用 script 标签引入了外站的 JS 文件,之所以可以这样做,是因为同源策略并不会限制这种行为。攻击者利用的同样也是这一点,构造一个恶意页面,直接用 script 标签引入包含受害者隐私数据的跨域资源,当受害者访问恶意页面时,浏览器就会自动请求对应资源,同时会带上相关 cookie,这样恶意页面就拿到了受害者的隐私数据。具体的可以参考:CROSS-SITE SCRIPT INCLUSION - A FAMELESS BUT WIDESPREAD WEB VULNERABILITY CLASS(https://www.scip.ch/en/?labs.20160414)
启用 IBC 之后,session cookie 的 SameSite 属性默认值变为 Lax,恶意页面跨域请求包含受害者隐私数据的资源时不再携带 session cookie,相当于以未登录状态访问隐私数据,肯定会失败。
JSONP 泄露
JSONP 是一种跨域通信机制。可以把 JSONP 泄露看成是 XSSI 攻击的一种形式。
测试站点 www.foo.com 包含一个 JSONP 接口,输出的是当前用户的邮箱。
PoC 如下:先自定义一个用于接受数据的 evil 函数,然后把函数名通过 cb 参数传递给 JSONP 接口
启用 IBC 后,Burp 拦截到的请求如下:JSONP 请求未携带 cookie,服务端提示需要登录
但是这时候,不仅恶意网站无法利用 JSONP 接口窃取数据,就连 JSONP 接口原本的使用方也无法正常获取数据了。所以,为了使 JSONP 接口正常工作,开发者需要显式的将相关 cookie 设置为 SameSite=None。
把 sessionId cookie 的 SameSite 设置成 None 后再次测试:JSONP 泄露又可以被利用了。
总结一下:由于 JSONP 本意就是为了进行跨站数据传递,所以开发者会显式的将相关 cookie 设置为 SameSite=None,对于这样的 JSONP 接口,数据泄露的风险依然存在。对于那些开发者无意识造成的 JSONP 泄漏点,IBC 可以提供一定程度的保护。
XSLeaks
XSLeaks 和 XSSI 类似,都是用来跨域获取受害者的隐私数据,只不过 XSLeaks 利用的是浏览器的 side channel,例如:HTTP 响应的耗时等等。具体的可以参考:https://github.com/xsleaks/xsleaks/ 。
启用 IBC 之后,session cookie 的 SameSite 属性默认值变为 Lax,用来探测用户隐私数据的跨域请求不携带 session cookie,导致无法获取那些需要登录才能访问的隐私数据。
但是需要指出的是,一些特定的侧信道技术,例如:通过 window.open() 的,可能依然有效,因为这属于安全的跨站顶级跳转。
Data Exfiltration
这里的 Data Exfiltration 指的是利用不同技术手段绕过同源策略进而提取目标数据。例如:
CVE-2015-1287、CVE-2015-5826 通过 CSS 跨域提取数据(https://blog.innerht.ml/cross-origin-css-attacks-revisited-feat-utf-16/)
CVE-2014-3160 通过 SVG 绕过 Chrome 同源策略(https://christian-schneider.net/ChromeSopBypassWithSvg.html)
Data Exfiltration 成功的前提也是跨域请求自动携带 session cookie,进而提取隐私数据。启用 IBC 后,这个前提不再成立,攻击也就不再有效。
CORS(Cross-Origin Resource Sharing)配置错误
CORS 允许 Web 应用服务器进行跨域访问控制,服务器通过响应头来控制哪些来源的请求可以访问自身资源,从而使跨域数据传输可以安全进行。
但是从另一个角度看:CORS 相当于在同源策略这堵墙上开了一扇窗。一旦开发者没有正确配置 CORS 响应头,就会让攻击者有机可乘。
常见的错误配置有:
开发者未做任何验证,直接把请求头里的 Origin 原样输出到了 Access-Control-Allow-Origin 响应头
开发者使用正则表达式对 Origin 进行判断后将其输出到 Access-Control-Allow-Origin 响应头,但是正则表达式写的不完备,存在绕过
把 Access-Control-Allow-Origin 响应头的值设置成 null
如果出现上述错误配置,攻击者就可以跨域发起请求,并携带认证 cookie,访问目标资源。
开发者使用 CORS 和 JSONP 的目的都是为了跨站数据传输。IBC 对二者的影响也类似,启用 IBC 后,要使 CORS 正常工作,需要显式的将跨站请求用到的 cookie 设置为 SameSite=None。所以 IBC 对 CORS 配置错误漏洞影响不大。
XSS
IBC 对 XSS 漏洞本身影响不大,影响的主要是一些利用方式,比如常见的:通过 iframe 嵌入目标网站来触发其 XSS。
测试站点 www.foo.com 在个人资料页(需要登录才能访问)存在一个 XSS,直接把 URL 参数 name 输出到页面里。
PoC 如下:
<iframe frameborder="0"></iframe>
未启用 IBC 时:成功弹 cookie
启用 IBC 后:由于 iframe 嵌入目标网站属于跨站请求,没有携带 session cookie,嵌进来的页面就成了未登录状态,导致 XSS 利用失败。
点击劫持
点击劫持成功的前提是:能够在攻击者的页面中嵌入目标网站,同时受害者在目标网站是已登录状态。
和上面通过 iframe 触发 XSS 一样,由于嵌入目标网站的请求属于跨站请求,启用 IBC 之后,session cookie 的 SameSite 属性默认值变为 Lax,跨站请求不携带 session cookie,导致实际嵌进来的页面是未登录状态,也就没办法进行敏感操作,点击劫持失去意义。
需要指出的是,如果目标网站把认证信息 session ID,access tokens 之类的保存到了本地存储(localStorage 或是 sessionStorage),并不依赖 cookie,那么点击劫持依然有效。
总结
这里借用大牛 @filedescriptor 整理的表格做个总结:
需要说明的是:不要把上面的讨论当成结论,具体漏洞还需要具体分析。不同漏洞的变种、利用条件、组合方式的差异可能会带来不一样的结果。举个例子:假设攻击者的目标站点是 www.foo.com, 如果攻击者能够通过某种方式(脚本注入,甚至只是一个 HTML 注入)绕过同源策略,进入目标站点的兄弟域或是子域的上下文,那么也就相当于进入了同站的范围内,此时 SameSite 属性就起不到任何限制了。
各个 cookie 的 SameSite 要使用哪个值,需要根据具体业务、使用场景来进行选择,同时还要考虑安全性和用户体验。
简单的,如果只想将 cookie 的访问限制在自己的网站里,应该选择使用 SameSite=Strict 来阻止跨站使用。
但是全部使用 SameSite=Strict,可能会带来一些用户体验上的问题。例如:著名社区土司比较注重用户的安全,所以将整站的 Cookie 都设置成了 SameSite=Strict。但这样会导致很多从外站点击超链接跳转到土司的用户无法正常查看帖子,可能需要重新登录。
所以,如果你希望从其它网站(例如:百度、邮箱)通过点击超链接跳转过来的用户的登录状态不丢失,就需要考虑使用 SameSite=Lax。
如果你的网站需要和其它网站在前端进行数据交换,或者深度融合(可以嵌入到其它网站),那么就得考虑使用 SameSite=None。
使用 SameSite=None 时还要注意,有一些客户端并不支持或存在 Bug,例如:旧版本的 Safari 会把 SameSite=None 当做 SameSite=Strict 来处理。详情可以参考:https://www.chromium.org/updates/same-site/incompatible-clients
总结
把 SameSite 的默认值调整为 Lax,符合 secure-by-default 原则,也在一定程度上减少了 Web 应用程序的攻击面,提升了用户的安全性,也有助于保护用户的隐私。
其实,不仅仅是 IBC 对 cookie 的改善,各个主流浏览器也在不断收紧第三方 cookie 的使用,例如:Safari 浏览器智能防追踪(Intelligent Tracking Prevention)功能,火狐浏览器的增强防追踪(Enhanced Tracking Prevention)功能等都在帮助用户朝着更安全,隐私性更好的 web 迈进。
https://web.dev/samesite-cookies-explained/
https://blog.reconless.com/samesite-by-default/
https://www.chromium.org/updates/same-site/faq