杜绝CSRF的方案:SameSite cookies
/0x00 前言
上周和小伙伴讨论了产品线没法快速完成一套Anti-CSRF Token机制来防御CSRF,因为涉及到的改动较大,这里校验Referer头也是存在被绕过的风险的,因此基于此场景给产品线提出SameSite cookies的安全解决方案来杜绝CSRF。
0x01 SameSite
众所周知,正是Cookie的滥用,才导致了CSRF漏洞的存在。
在Cookie出现SameSite属性之前,针对CSRF攻击的防御措施都是基于Anti-CSRF Token机制或者校验Referer头字段。
从Chrome 51开始,浏览器的Cookie新增加了一个SameSite属性,用来防止CSRF攻击和用户追踪(当然也能防御XSSI)。
SameSite 是HTTP响应头 Set-Cookie 的属性之一。它允许您声明该Cookie是否仅限于第一方或者同一站点上下文。
其中可以设置如下三个属性值:
- Strict
- Lax
- None
Strict
Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
1 | Set-Cookie: CookieName=CookieValue; SameSite=Strict; |
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
Lax
Lax规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
1 | Set-Cookie: CookieName=CookieValue; SameSite=Lax; |
导航到目标网址的 GET 请求,只包括三种情况:链接,预加载请求,GET 表单。详见下表。
请求类型 | 示例 | 正常情况 | Lax |
---|---|---|---|
链接 | <a href="..."></a> |
发送 Cookie | 发送 Cookie |
预加载 | <link rel="prerender" href="..."/> |
发送 Cookie | 发送 Cookie |
GET 表单 | <form method="GET" action="..."> |
发送 Cookie | 发送 Cookie |
POST 表单 | <form method="POST" action="..."> |
发送 Cookie | 不发送 |
iframe | <iframe src="..."></iframe> |
发送 Cookie | 不发送 |
AJAX | $.get("...") |
发送 Cookie | 不发送 |
Image | <img src="..."> |
发送 Cookie | 不发送 |
设置了Strict或Lax以后,基本就杜绝了 CSRF 攻击。当然,前提是用户浏览器支持 SameSite 属性。
None
Chrome 计划将Lax变为默认设置。这时,网站可以选择显式关闭SameSite属性,将其设为None。不过,前提是必须同时设置Secure属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
下面的设置无效。
1 | Set-Cookie: widget_session=abc123; SameSite=None |
下面的设置有效。
1 | Set-Cookie: widget_session=abc123; SameSite=None; Secure |
0x02 各版本浏览器支持情况
SameSite的由于是后来的产物,因此其不足之处在于不同浏览器支持的情况各异。
如图:
具体情况可看:https://caniuse.com/?search=SameSite
0x03 安全整改建议
一般而言,对于业务来说是建议设置SameSite属性值为Lax的,因为Strict太影响用户体验。
Lax对GET请求是放行的,因此整改的重点在于要严格区分GET和POST的职责,即GET只能进行一些查询类或导航类的访问、而不是进行状态更改,要执行一些更改类的表单操作就必须交由POST来处理,在这种场景下Lax的设置才会将风险降到较低。这是因为:
- 如果用GET携带参数访问,其中的参数值将会记录在浏览器历史、Web日志以及访问其他页面的Referer头字段中;
- Cookie的SameSite属性设置为Lax的GET请求还是会被攻击者利用进行CSRF攻击,且GET型CSRF攻击难度低;
除此之外,还需要考虑客户端使用的浏览器版本过低或者非常见浏览器使得SameSite失效的问题。
当然,结合其他的Cookie头字段设置可以达到更高的安全性,可参考:https://scotthelme.co.uk/tough-cookies/
0x04 各语言设置SameSite例子
设置SameSite其实就是对响应报文Set-Cookie头加上对应的SameSite属性键值对而已,这里只给出几种最常用的Web开发语言的设置示例。
其他类型语言可以参考:https://github.com/GoogleChromeLabs/samesite-examples
Java设置SameSite
在Java Web中,使用setHeader()直接添加对应的Set-Cookie头的值即可:
1 | boolean firstHeader = true; |
PHP设置SameSite
PHP >= 7.3 版本
PHP 7.3及以上版本的setcookie()函数已经支持samesite属性,并且允许None为有效值。
1 | // Set a same-site cookie for first-party contexts |
PHP < 7.3 版本
在低版本PHP中,可以直接通过header()函数设置Set-Cookie
头的值来设置samesite:
1 | // Set a same-site cookie for first-party contexts |
Session Cookie
对于Session Cookie,可以在session_set_cookie_params()方法中设置。PHP 7.3.0为samesite引入了新属性:
1 | if (PHP_VERSION_ID >= 70300) { |
Python设置SameSite
Python原生
在Python 3.8中http.cookie
已支持SameSite属性。
低于该版本的需要直接在setcookie中设置。
Flask框架
第一种方式:
1 | from flask import Flask, make_response |
如果上述报错,可替换如下第二种方式:
1 | from flask import Flask, make_response |
0x05 实践效果测试
以DVWA的Low级别CSRF靶场为效果测试Demo。
默认情况下,登录成功之后,在Chrome中看到Cookie中各项值的SameSite是空的:
在Low级中,CSRF是GET型的,可以用如下PoC打:
1 | <a href="http://127.0.0.1/dvwa/vulnerabilities/csrf/?password_new=mi1k7ea&password_conf=mi1k7ea&Change=Change">Click me</a> |
修改DVWA代码中设置Cookie的地方,以前面PHP Session Cookie的方式只给PHPSESSID部分添加SameSite属性,值为Strict:
此时再登录就看到对应的Cookie值PHPSESSID被设置了SameSite属性:
此时再用前面的PoC打,是失败的即重定向到login页面,看到发起CSRF攻击的Cookie只是带了security属性值并没有PHPSESSID的属性值,被SameSite成功防御住了:
但是,如果这种业务场景下设置SameSite为Lax的话,是不会拦截GET型CSRF的:
因此,需要根据产品自身的业务场景来决定采用Strict或Lax。