Better

Ethan的博客,欢迎访问交流

Node防范CSRF遇到的小阻碍和疑问

由于在自己博客上,有一个找我功能,是不需要登录便可以给我发送消息的功能,讲道理对于我这种个人把玩的博客,基本上很低频的功能才对,结果好不热闹,再加上发送消息时会有邮件提醒,但是却都是些乱七八糟的东西,所以非常恼火,我就在想为什么会这样呢?想起来之前查看日志时一些莫名其妙的请求,会不会是机器人胡乱请求的呢?因此就想到了CSRF攻击,加入csrfToken是否可以解决这个问题呢?

初探

考虑到不想引入额外的复杂度,并没有引入三方库,而是自己写了一个简单的token生成和校验,简单代码如下

router.get('/', function (req, res, next) {
    var csrfToken = parseInt(Math.random() * 999999999, 10)
    res.cookie('_csrf', csrfToken)
    res.render('contact', {
        csrfToken
    });
});

上面的代码很简单,就是生成一个较大的随机数,写入cookie和页面的隐藏域。接着写校验部分

router.post('/', function(req, res, next) {
    // 项目中使用的formidable中间件,而不是body-parser
    var _csrf = req.fields._csrf;
    var _csrfToken = req.cookies._csrf;
    // 防御CSRF攻击
    if(!_csrf || !_csrfToken || _csrf !== _csrfToken) {
        throw new Error('非法请求');
    }
    // ...
})

校验部分同样十分简单,在body中取出_csrf,在cookie中取出正确的_csrfToken,如果两者不存在或者不一致,就认为这是非法请求,直接报错。

根据CSRF的攻击原理,加上我目前的粗浅理解,这样其实就可以达到效果,什么效果呢?就是用户必须到达我网站页面,才能发送合法请求,否则会被拦截。因此开心的发布代码,重启服务,以为不会在有机器人烦我了。

可是事与愿违,马上同样的事情发生了。

思考

为什么我的方法会失效呢?代码是没有问题了,通过日志查看,每次确实有正确的token提交,so what?难道是我的token太简单了,于是今天的我(2018/11/06)准备死马当活马医,引入csurf模块,增大token的复杂度。但却并不像我想的那么顺利。

简单代码如下:

var csrfProtection = require('csurf')()
router.get('/', function (req, res, next) {
    res.render('contact', {
        csrfToken: req.csrfToken()
    });
});
router.post('/', csrfProtection, function(req, res, next) {
    // ...
})

首先我们全部使用默认配置,却一直提示invalid csrf token,这是为何,原因还是在于我没有使用body-parser的原因,因为他默认取值顺序为

  • req.body._csrf
  • req.query._csrf
  • req.headers['csrf-token']
  • req.headers['xsrf-token']
  • req.headers['x-csrf-token']
  • req.headers['x-xsrf-token']

所幸作者考虑到他不可能涵盖所有情况,提供了一个配置,让我们可以自定义函数或者_csrf值,修改代码如下

var csrfProtection = require('csurf')({
    value: function(req) {
        return req.fields._csrf
    }
})

这样一来问题解决了,原理应该是通过获取到的_csrf值与服务器端session存储的csrfToken进行比较。

options还有更多的配置,比如我在实践中用到cookie配置,默认为false,不存储cookie,设置为true后,我发现cookie中的值和hidden隐藏域中的值不一致,校验却可以通过,而且我修改任一方的值,校验就会不通过,说明是有生效的,但不一致是因为什么呢?让我想对他了解更多了!

深入

首先我们深入分析一下csurf函数执行后,返回一个中间件函数,我们先看看csurf做了什么,csurf依赖csrf模块,用于处理token的生成与验证

  • 读取配置,有传入配置个更新配置
    • cookie配置,默认undefined
    • sessionKey,默认session
    • value函数,默认上节所言
    • ignoreMethods,默认['GET', 'HEAD', 'OPTIONS']
  • 返回中间件函数csrf
    • 检查配置,是否支持cookie或session
    • req追加csrfToken函数,负责返回token
    • 验证token

我们主要了解下验证token的逻辑,就可以知道为啥cookie中的值和hidden隐藏域中的值不一致了,具体代码在csrf的verify中,代码如下

Tokens.prototype.verify = function verify (secret, token) {
  if (!secret || typeof secret !== 'string') {
    return false
  }

  if (!token || typeof token !== 'string') {
    return false
  }

  var index = token.indexOf('-')

  if (index === -1) {
    return false
  }

  var salt = token.substr(0, index)
  var expected = this._tokenize(secret, salt)

  return compare(token, expected)
}

参数token为隐藏域中的值,secret为session或cookie中正确的值,从代码中可以发现,取token中-之前的部分作为salt和secret重新生成值,答案就出来了,为了就是防篡改,防伪造。

总结

可见csurf比我的屌丝版还厉害许多,具体如下

  1. 支持cookie和session
  2. token更复杂
  3. 加入防篡改,防伪造

不过折腾了这么久,回到最初的问题,能不能解决我的苦恼,还是要看接下来的效果了

2018/11/08追加:经过两天的观察,并没有什么卵用,垃圾信息还是一堆,该如何解决呢



留言