在说laravel框架里对CSRF的攻击防护之前先对XSS和CSRF攻击做一下简单的介绍。
XSS和CSRF攻击
XSS全称“跨站脚本”攻击(Cross-site scripting)。是通过利用网站的bug在页面里注入恶意的js代码,当用户打开这些页面的时候,这些恶意代码就会在用户毫无感知的情况下执行,从而实现一些用户未经授权的操作,例如转账汇款,更改密码等。
实施攻击的比较常见的方式是通过发帖或评论等方式上传数据到服务器,比如发布一条包含这些数据的评论:<script type="text/javascript">alert('xss')</script>
,如果服务器没有过滤或转义上传的这些评论数据,直接就存入数据库。那么当这个页面再次被请求的时候,这条评论会从数据库取出,展示在前端页面上,html会将<script>标签中的脚本解析执行,完成xss攻击。
目前主流的浏览器都具有一定的xss防护机制,这种类型的xss可能在浏览器会直接过滤掉。这里主要说明攻击原理。
想象一下如果脚本的内容不是alert('xss')
,而是调用发起转账接口或是将用户的cookies上传到攻击者的服务器上(前提是cookies没有设置http-only),那用户将会在毫不知情的情况下造成损失。
CSRF叫做跨站请求伪造(Cross-site request forgery),本质上来说XSS是实现CSRF攻击的一种手段,它们都属于跨站攻击。
laravel中CSRF的防护
在laravel中,框架是通过发送令牌的方式来实现CSRF防护的。具体的实现过程是,当浏览器发起页面请求的时候,laravel会返回给浏览器一个额外的cookie,cookie名为XSRF-TOKEN
,值是一个随机的字符串,且每一次请求都会变化。
当用户在浏览器中通过点击按钮或其他方式发起POST请求时,浏览器会自动将cookies发送给后端,laravel通过反序列化session_id,拿到用户session数据,然后对比用户session中的_token
和XSRF-TOKEN
cookie的值是否匹配,如果不匹配,会返回419
返回码。
注意,laravel对于
GET
请求是不做CSRF验证的。
通过这种机制,就能够防止非浏览器环境对非GET接口的直接调用。
防护的局限性
想象一下XSS攻击的实现场景。如果页面已经被注入了js,那么通过js获取XSRF-TOKEN
cookie不就是轻而易举的事情了吗?这样子不照样可以发起恶意请求了?
确实会存在这种风险,所以在github上就有人提议将XSRF-TOKEN
这个cookie设置为http-only,以防止这种情况发生。
cookie的http-only属性设置为true后,可以阻止JavaScript的访问
不过这个请求最终没有被实施。刚开始我也不太理解,后来想想也确实没有必要,而且也不应该这样做,因为这个令牌就是用于给js读取的,然后js发送ajax请求之前读取出此值,然后添加上X-CSRF-TOKEN
头部。laravel的验证机制就是基于此头部的,而不是从cookie中获取的值。如下是源码:
/**
* Get the CSRF token from the request.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function getTokenFromRequest($request)
{
$token = $request->input('_token') ?: $request->header('X-CSRF-TOKEN');
if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
$token = $this->encrypter->decrypt($header, static::serialized());
}
return $token;
}
并且存放用户session_id
的这个cookie是http-only的,虽然恶意的js脚本可以获取到XSRF-TOKEN
令牌,但是它获取不到用户的session_id
,这样子即使有令牌在手,你也无法假扮用户执行非授权操作。
http-only是框架的默认配置,你也可以更改
session.php
配置文件,将其改为false,不过为了网站的安全性,强烈不建议更改它。
关于此点的更详细解释,可以查看我的这篇博文
新的攻击方式
现在看下来好像只需要保证session_id
是http-only的,那么我们就可以高枕无忧,无须担心xss注入了。既然恶意的js无法获取到我的session_id
,无法冒充我本人,那我还有什么好担心的呢?
其实不然,我认为还有两点需要担心:
其一,通过http协议发送出去的数据包有可能被中间人攻击,被截获,然后攻击者从截获的数据包中解析出用户的cookies,接下来再冒充用户伪造请求发起攻击。
此时,可以将站点升级成https,然后将存有session_id
的这个cookie设置为secure
,即session.php
配置文件中的secure
配置项。
cookie的secure属性表示只有当协议是https时,浏览器才会传输此cookie给后端,否则就不发送。
其二,这里假设攻击者将受害站点B
嵌入一个iframe中,然后你通过攻击者的诱骗或其他不知情的方式访问到了攻击者站点A
,并发起了http请求(点击了按钮或其他什么形式触发的),表面上看此请求是在A
站点发出的,实际上可能你点击的那个按钮是B
站点的一个转账按钮,攻击者通过iframe
嵌入B
站点的形式,把那个按钮包装成了A
站点下看起来无害的一个按钮,并诱使你点击了它。如果你此时在站点B
是属于登陆状态的话,那么这次请求浏览器或自动将你的cookies发送给后端,达到了攻击目的。
对此的防护手段,就是防止页面被iframe。有如下几种方式来防止页面被未经授权的iframe:
- 使用js代码来判断:
if(window != window.top){
window.top.location.href = myOrigin;
}
注意:这个只判断top是不太准确的,可能循环嵌套。比如:your-web -> attacker-web -> your-web。这种就判断失败了
-
后端返回一个
X-Frame-Options: deny
头部,此方案可以在运维端配置,通过配置nginx来实现。X-Frame-Options
头部的值还可以设置为其他的,具体参考文末的参考资料。 -
通过CSP(Content Security Policy)机制来实现。实现CSP机制也有两种方案:a. 通过
Content Security Policy
头部来控制。b. 通过在html代码中加上meta
标签的形式来控制,例如<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">
。
总结
综上所述,为了防范CSRF攻击,我们要做好如下三点:
- 对于用户提交的数据做过滤和转义,防止
<script>
标签被存入。 - 对于cookie设置http-only,阻止js访问之。
- 防止页面未经授权的iframe。
当然,各种攻击手段和漏洞层出不穷,也许还有其他的攻击形式我没有想到,或许这三种手段也无法做到尽善尽美的防护,欢迎大家一起来留言讨论研究。
我相信只有从原理上理解了这些攻击手段,才能从源头找到应对的策略,对于本文有表述的不到位的,或理解有误的地方也欢迎大家提出指正。
完!
参考资料:
MDN - X-Frame-Options
MDN - Content Security Policy (CSP)
laravel - CSRF Protection
[Proposal] Allow setting XSRF-TOKEN cookie as httpOnly for XSS attacks
wikipedia - Cross-site request forgery
CSRF令牌为什么要通过HTTP头部而不是cookie来验证
网友评论