倒霉蛋李建国
brunoyang opened this issue · 2 comments
李建国是个倒霉蛋,小时候爬树摔断腿,考试抄错题;长大了骑车被碰瓷,吃饭吃出钢丝球。
一天,李建国打开了某网银要转1000块给王红霞,转账当然是要登录的。转完账后,李建国关闭了 tab 页。随后,李建国打开了不可描述的网站,半分钟后关闭了这个网站。突然,李建国的手机收到银行发来的两条短信,一条是转给王红霞的1000,另一条是不知道转给谁的10000。李建国慌了,他知道网银被盗了。他很疑惑,我啥也没干,咋就被盗了捏,难道见鬼了。
当然,李建国没见鬼,他只是碰上了 CSRF 漏洞。
所谓 CSRF(Cross-site request forgery),跨站攻击,指的是攻击者盗用你的身份,向某个网站发起恶意请求。
我们看李建国的例子,被盗分这么几步:
- 浏览并登录网银,在网银的域名下留下 cookie 信息;
- 发起转账,假设银行转账接口是 GET 请求的
http://bank.com/transfer/1000/to/wang-hong-xia
; - 浏览恶意网站,恶意网站上有张图
<img src="http://bank.com/transfer/10000/to/huai-yin" />
; - 该请求带上还未过期的 cookie 信息,将10000转给了坏银。
这个漏洞能够成立,基于以下事实:
- 服务器是通过请求中的 cookie 信息辨认用户的;
- 浏览器关闭tab页,并不会立即清除保存在本地的 cookie, 同时服务器上保留的会话信息在过期之前不会清除,除非用户关闭tab之前主动登出;
- 在B页面上发起A页面上的请求,该请求会带上A域名下的 cookie。
有了以上的信息,CSRF 漏洞就能成立了。
银行知道了该信息后,紧急组织专家堵漏洞。
银行的转账接口使用 GET 请求,这是严重的错误,因为 GET 请求应该是幂等的,不管调用多少次都是一个结果。于是把银行把接口升级为 POST 请求。
恶意网站也升级了,每次访问都会发起 POST 请求。李建国又被盗了10000。
银行知道了该信息后,又紧急组织专家堵漏洞。
这次他们开始校验请求头中的 referer 信息,因为 referer 信息记录了该请求从哪个域名发出。
恶意网站又升级了,这次他们加了一个代理服务器,在请求发给银行之前先通过代理服务器修改referer信息。李建国再次被盗了10000。
银行知道了该信息后,再次紧急组织专家堵漏洞。
这次他们给表单上增加一个隐藏域,<input type="hidden" value="23kh4acsdudesfr45hoiad" name="ctoken" />
,在每次表单提交时都带上 ctoken。
恶意网站发现升级也没用了,就去寻找下一个漏洞了……
防范 CSRF
防范 CSRF 最有效的方式,就是每次提交都要求手动输入验证码,但这样的用户体验很差。现在应用最广泛的就是为提交的表单增加伪随机字段。在服务器上生成一串随机字符串,带到页面上并把随机码保存起来。在用户提交回的表单中取出随机码并与服务器上保存的作对比,如果匹配,那就是合法的请求;要是不匹配,就可以认为这是一个非法的请求。
koa-csrf
基于 koa-csrf@2.4.0。以下 koa-csrf 简称为kcsrf。
先来看最基本用法
const koa = require('koa');
const csrf = require('koa-csrf');
const session = require('koa-generic-session');
const app = koa();
app.keys = ['session secret'];
app.use(session());
csrf(app);
app.use(csrf.middleware);
app.use(function* () {
if (this.method === 'GET') {
this.body = this.csrf;
} else if (this.method === 'POST') {
this.status = 204;
}
});
app.listen(3000);
kcsrf 依托于 session,所以我们引入了koa-generic-session
。kcsrf 接受一个配置项,可以传入 saltLength 和 secretLength,分别为盐长度和token长度。这两个参数被透传给 csrf 模块,用于生成 token。
该模块很简单,通过定义一个 getter 方法,通过如下形式传给模板,用于生成html页面。
app.use(function *() {
this.render({
csrf: this.csrf
});
});
而在 getter 方法内部,生成并返回token的同时,还往 session 上增加了一个 secret 字段,用以保存生成的token。
在第二次请求过来时,就会将 token 带回来,位置可以在表单、查询串或自定义头上,将请求中的 token 取出与 session.secret 作对比,就可以判断是否为跨站攻击。
解释的很清楚,hah~~
叙述的方式很有趣