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
2
3
4
5
6
7
8
9
10
11
boolean firstHeader = true;
for (String header : cookiesHeaders) {
if (firstHeader) {
httpResponse.setHeader("Set-Cookie",
String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
httpResponse.addHeader("Set-Cookie",
String.format("%s; %s", header, "SameSite=Strict"));
}

PHP设置SameSite

PHP >= 7.3 版本

PHP 7.3及以上版本的setcookie()函数已经支持samesite属性,并且允许None为有效值。

1
2
3
4
// Set a same-site cookie for first-party contexts
setcookie('cookie1', 'value1', ['samesite' => 'Lax']);
// Set a cross-site cookie for third-party contexts
setcookie('cookie2', 'value2', ['samesite' => 'None', 'secure' => true]);

PHP < 7.3 版本

在低版本PHP中,可以直接通过header()函数设置Set-Cookie头的值来设置samesite:

1
2
3
4
// Set a same-site cookie for first-party contexts
header('Set-Cookie: cookie1=value1; SameSite=Lax', false);
// Set a cross-site cookie for third-party contexts
header('Set-Cookie: cookie2=value2; SameSite=None; Secure', false);

对于Session Cookie,可以在session_set_cookie_params()方法中设置。PHP 7.3.0为samesite引入了新属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (PHP_VERSION_ID >= 70300) { 
session_set_cookie_params([
'lifetime' => $cookie_timeout,
'path' => '/',
'domain' => $cookie_domain,
'secure' => $session_secure,
'httponly' => $cookie_httponly,
'samesite' => 'Lax'
]);
} else {
session_set_cookie_params(
$cookie_timeout,
'/; samesite=Lax',
$cookie_domain,
$session_secure,
$cookie_httponly
);
}

Python设置SameSite

Python原生

在Python 3.8中http.cookie已支持SameSite属性。

低于该版本的需要直接在setcookie中设置。

Flask框架

第一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/')
def hello_world():
resp = make_response('Hello, World!')
# Set a same-site cookie for first-party contexts
resp.set_cookie('cookie1', 'value1', samesite='Lax')
# Set a cross-site cookie for third-party contexts
resp.set_cookie('cookie2', 'value2', samesite='None', secure=True)
return resp

如果上述报错,可替换如下第二种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/')
def hello_world():
resp = make_response('Hello, World!')
# Set a same-site cookie for first-party contexts
resp.set_cookie('cookie1', 'value1', samesite='Lax')
# Ensure you use "add" to not overwrite existing cookie headers
# Set a cross-site cookie for third-party contexts
resp.headers.add('Set-Cookie','cookie2=value2; SameSite=None; Secure')
return resp

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。

0x06 参考

Cookie 的 SameSite 属性