brunoyang / blog

134 stars 13 forks source link

倒霉蛋李建国 #17

Open brunoyang opened 8 years ago

brunoyang commented 8 years ago

李建国是个倒霉蛋,小时候爬树摔断腿,考试抄错题;长大了骑车被碰瓷,吃饭吃出钢丝球。

一天,李建国打开了某网银要转1000块给王红霞,转账当然是要登录的。转完账后,李建国关闭了 tab 页。随后,李建国打开了不可描述的网站,半分钟后关闭了这个网站。突然,李建国的手机收到银行发来的两条短信,一条是转给王红霞的1000,另一条是不知道转给谁的10000。李建国慌了,他知道网银被盗了。他很疑惑,我啥也没干,咋就被盗了捏,难道见鬼了。

当然,李建国没见鬼,他只是碰上了 CSRF 漏洞。

所谓 CSRF(Cross-site request forgery),跨站攻击,指的是攻击者盗用你的身份,向某个网站发起恶意请求。

我们看李建国的例子,被盗分这么几步:

  1. 浏览并登录网银,在网银的域名下留下 cookie 信息;
  2. 发起转账,假设银行转账接口是 GET 请求的http://bank.com/transfer/1000/to/wang-hong-xia
  3. 浏览恶意网站,恶意网站上有张图<img src="http://bank.com/transfer/10000/to/huai-yin" />;
  4. 该请求带上还未过期的 cookie 信息,将10000转给了坏银。

这个漏洞能够成立,基于以下事实:

  1. 服务器是通过请求中的 cookie 信息辨认用户的;
  2. 浏览器关闭tab页,并不会立即清除保存在本地的 cookie, 同时服务器上保留的会话信息在过期之前不会清除,除非用户关闭tab之前主动登出;
  3. 在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 作对比,就可以判断是否为跨站攻击。

mlyknown commented 8 years ago

解释的很清楚,hah~~

tonyjiafan commented 7 years ago

叙述的方式很有趣