CSRF 介绍
CSRF,是跨站请求伪造(Cross Site Request Forgery)的缩写,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。
通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。
CSRF 攻击示例
这里有一个网站,用户可以看文章,登录之后可以发评论。
如果用户是登录状态,打开了这样的页面,
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>csrf攻击</title>
<meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" name="viewport" />
</head>
<body style="display: none;">
<form target="myIframe" id="csrf" action="https://www.kkkk1000.com/csrf/data/post_comment.php" metdod="POST">
<input type="text" name="content" value="csrf攻击" />
</form>
<!-- iframe 用来防止页面跳转 -->
<iframe id="myIframe" name="myIframe"></iframe>
<script>
var form = document.querySelector('#csrf');
form.submit();
</script>
</body>
</html>
就会自动在文章下发一条评论,这样就算完成了一次 CSRF 攻击。
当然,如果你把这个页面放服务器上,然后做成一个链接,用户点击这个链接,同样可以完成攻击。
从图中可以看出,右边和左边的页面是在不同站点下的,用户打开的右边的空白页,就偷偷提交了一条评论,刷新左边的页面也确实看到了刚刚提交的评论。
我们来看看,这次的攻击是怎么成功的。
首先我们要知道一些关于 Cookie 的知识。
HTTP Cookie(也叫 Web Cookie或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
好的,我们继续往下说。
在发评论的时候,提交评论的请求会带上这个凭证,后端通过判断这个凭证,来确认是哪个用户。
登录时,设置 Cookie:
提交评论时,携带 Cookie:
我们再来看看,发起攻击的页面里的内容。
<body style="display: none;">
<form target="myIframe" id="csrf" action="https://www.kkkk1000.com/csrf/data/post_comment.php" metdod="POST">
<input type="text" name="content" value="csrf攻击" />
</form>
<!-- iframe 用来防止页面跳转 -->
<iframe id="myIframe" name="myIframe"></iframe>
<script>
var form = document.querySelector('#csrf');
form.submit();
</script>
</body>
这些代码的意思就是,当用户点击这个链接,会自动提交 form 表单,而这个表单就是用来提交评论的,提交评论请求需要的参数,在 form 表单中也都已经准备好了,如果用户登录过网站,Cookie 中存储的用户的凭证,会随着请求一起传到服务器端。所以服务器端就会认为这是用户要提交一条评论。
CSRF 特点
-
攻击一般发起在第三方网站,而不是被攻击的网站。
-
攻击是利用受害者在被攻击网站的登录凭证,冒充受害者提交操作,仅仅是“冒用”,而不是直接窃取数据。
-
攻击者预测出被攻击的网站接口的所有参数,成功伪造请求。
防御方法
SameSite 属性
Cookie 的 SameSite 属性用来限制第三方 Cookie,从而减少安全风险,可以用来防止 CSRF 攻击和用户追踪。
它可以设置三个值。
-
Strict
-
Lax
-
None
Strict
Strict最为严格,完全禁止第三方 Cookie,跨站点时,任何情况下都不会发送 Cookie。换言之,只有当前网页的 URL 与请求目标一致,才会带上 Cookie。
Set-Cookie: CookieName=CookieValue; SameSite=Strict;
这个规则过于严格,可能造成非常不好的用户体验。比如,当前网页有一个 GitHub 链接,用户点击跳转就不会带有 GitHub 的 Cookie,跳转过去总是未登陆状态。
Lax
Lax 规则稍稍放宽,大多数情况也是不发送第三方 Cookie,但是导航到目标网址的 Get 请求除外。
Set-Cookie: CookieName=CookieValue; SameSite=Lax;
Chrome 计划将 Lax 变为默认设置。这时,网站可以选择显式关闭 SameSite 属性,将其设为 None 。不过,前提是必须同时设置 Secure 属性(Cookie 只能通过 HTTPS 协议发送),否则无效。
下面的设置无效:
Set-Cookie: widget_session=abc123; SameSite=None
下面的设置有效:
Set-Cookie: widget_session=abc123; SameSite=None; Secure
要使用 SameSite 属性,前提是用户浏览器支持 SameSite 属性,可以使用 caniuse 查看浏览器对于 SameSite 属性的支持。
同源检测
在 HTTP 协议中,每一个异步请求都会携带两个 Header ,用于标记来源域名:
-
Origin Header
-
Referer Header
这两个 Header 在浏览器发起请求时,大多数情况会自动带上,并且不能由前端自定义内容。 服务器可以通过解析这两个 Header 中的域名,确定请求的来源域。
通过校验请求的该字段,我们能知道请求是否是从本站发出的。我们可以通过拒绝非本站发出的请求,来避免了 CSRF 攻击。
验证 Origin
如果 Origin 存在,那么直接使用 Origin 中的字段确认来源域名就可以。
但是 Origin 在以下两种情况下并不存在:
-
1、 IE11同源策略: IE 11 不会在跨站 CORS 请求上添加 Origin 头,Referer 头将仍然是唯一的标识。最根本原因是因为IE 11对同源的定义和其他浏览器有不同,有两个主要的区别,可以参考 MDN Same-origin_policy#IE_Exceptions
-
2、 302重定向: 在302重定向之后 Origin 不包含在重定向的请求中,因为 Origin 可能会被认为是其他来源的敏感信息。对于302重定向的情况来说都是定向到新的服务器上的 URL ,因此浏览器不想将 Origin 泄漏到新的服务器上。
验证 Referer
如果 Referer 存在,也可以用来确认 HTTP 请求的来源地址。
需要注意的是在以下情况下 Referer 没有或者不可信:
-
1.IE6、7下使用
window.location.href=url
进行界面的跳转,会丢失 Referer。 -
2.IE6、7下使用
window.open
,也会缺失 Referer。 -
3.HTTPS 页面跳转到 HTTP 页面,所有浏览器 Referer 都丢失。
-
4.点击 Flash 上到达另外一个网站的时候,Referer 的情况就比较杂乱,不太可信。
总的来说,同源检测是一个相对简单的防范方法,能够防范绝大多数的 CSRF 攻击,但这并不是万无一失的,对于安全性要求较高,或者有较多用户输入内容的网站,我们就要对关键的接口做额外的防护措施。
验证码
验证码的种类有很多,比如图形验证码,基于人机之间知识差异的验证码,行为式验证码。
CSRF 攻击往往是在用户不知情的情况下成功伪造请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求,而且因为 CSRF 攻击无法获取到验证码,因此通常情况下,验证码能够很好地遏制 CSRF 攻击。
但验证码并不是万能的,因为出于用户体验考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。
添加 Token 验证
CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 Cookie 中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 Cookie 来通过安全验证。
要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 Token,并在服务器端建立一个拦截器来验证这个 Token,如果请求中没有 Token 或者 Token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
添加 Token 验证的步骤:
1、服务器将 Token 返回到前端
用户打开页面时,前端发起请求,服务器会返回一个 Token,该 Token 通过加密算法对数据进行加密,一般 Token 都包括随机字符串和时间戳的组合,同时 Token 会存在服务器的 Session 中。之后页面加载完成时,使用 JS 遍历整个 DOM 树,在 DOM 中所有地址是本站的 a
和 form
标签中加入 Token,其他的请求就在编码时手动添加 Token 这个参数。
2、前端发请求时携带这个 Token
对于 GET 请求,Token 将附在请求地址之后,这样 URL 就变成 http://url?token=tokenvalue
。 而对于 form
标签发起的 POST 请求来说,要在 form
的最后加上:
<input type=”hidden” name=”token” value=”tokenvalue”/>
总之,就是前端发请求时把 Token 以参数的形式加入请求中。
3、服务器验证 Token 是否正确
当前端得到了 Token ,再次提交给服务器的时候,服务器需要判断 Token 的有效性,验证过程是先解密 Token,对比加密字符串以及时间戳,如果加密字符串一致且时间未过期,那么这个 Token 就是有效的。
总结
CSRF能够攻击成功的本质是:重要操作的所有参数都是可以被攻击者猜测到的。
所以只要防止攻击者成功的构造一个伪造请求,就可以杜绝攻击了!