论node.js的express中csrf的随机验证失败的bug

来源:互联网 发布:js ajax 跨域访问 编辑:程序博客网 时间:2024/05/26 09:56

背景:

在node.js中直接使用express搭建文档结构,如果需要防护CSRF,需要在app.js中,也就是Middleware配置中,需要加上:

app.use(express.csrf());

问题:

当我加载,ejs的view的时候,如果使用在前端使用ajax再次请求,会获取到新的_csrf值,这原本是没有什么问题的,但是确会随机出现:

Error: Forbidden    at Object.exports.error (/Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/utils.js:62:13)    at createToken (/Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/middleware/csrf.js:82:55)    at Object.handle (/Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/middleware/csrf.js:48:24)    at next (/Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/proto.js:190:15)    at next (/Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/middleware/session.js:312:9)    at /Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/middleware/session.js:336:9    at /Users/apple/Desktop/groupbuy/node_modules/express/node_modules/connect/lib/middleware/session/memory.js:50:9    at process._tickCallback (node.js:415:13)

这明显是说因为csrf防护被拦截了。这简直就是奇葩情况。。。


原因:

前端通过不断的访问,发现一个问题,如图:



上图说明,express提供的csrf模块有漏洞,“6gidhoxMfXsLCuzLxAH+zd6+di9pFr6CxQ9MU=”这个字符被判断为匹配不上。


解决过程:

首先为了确认具体情况,深入模块代码(位于:/express/node_modules/connect/lib/middleware/csrf.js)进行研究,发现这个csrf模块是通过checkToken(token,secret)方法并使用createToken(salt,secret)来进行比对,代码块如下:

function createToken(salt, secret) {   return salt + crypto     .createHash('sha1')     .update(salt + secret)     .digest('base64'); }function checkToken(token, secret) {   return token === createToken(token.slice(0, 10), secret); }

为了验证问题,修改这个方法进行调试:
function checkToken(token, secret) {   if ('string' != typeof token) return false;   console.log("token:"+token);   console.log("createToken(token.slice(0,10),secret):"+createToken(token.slice(0,10),secret));   console.log(token===createToken(token.slice(0,10),secret));   return token === createToken(token.slice(0, 10), secret); }

出现异常的结果为:


token:RLaNpX4nMPvsv2 gjZ Rhe911/dDo0VA4Sa4A=secret:BHNfrSL2QRTiK-DgylWwCYUbcreateToken(token.slice(0,10),secret):RLaNpX4nMPvsv2+gjZ+Rhe911/dDo0VA4Sa4A=false
再次测试,正常为:

token:dyqRPL8L0ikahAeEMP2Wr/cNcD/hjPRUV0KlM=secret:BHNfrSL2QRTiK-DgylWwCYUbcreateToken(token.slice(0,10),secret):dyqRPL8L0ikahAeEMP2Wr/cNcD/hjPRUV0KlM=true

 可以清楚的看到,应该是”+“号的位置变成了“ ”

问题原因找到了,那为什么会发生这种情况?


真正的原因:

继续观察模块源代码,csrf.js是通过defaultValue(req)方法来获取到请求中的_csrf值,那修改这部分代码进行验证:

function defaultValue(req) {     console.log("body._csrf:"+req.body._csrf);     console.log("query._csrf:"+req.query._csrf);   return (req.body && req.body._csrf)     || (req.query && req.query._csrf)     || (req.headers['x-csrf-token'])     || (req.headers['x-xsrf-token']); }

会出现两种明显的结果:

body._csrf:undefinedquery._csrf:5c8lTNnuTB85FF  dVQFYsTpVA MmUgjhH7Yk=

body._csrf:7Ap38aDkBap+1kEvDguMM79/61ytKJYkDZVAY=query._csrf:undefined

这就证明,不知道是express还是node.js在对get方式的值里面的"+"字符解析不正常。

接着,继续寻找原因:

既然知道是query的原因,就到query.js里面去找原因,修改代码:

module.exports = function query(options){   return function query(req, res, next){     if (!req.query) {       req.query = ~req.url.indexOf('?')         ? qs.parse(parse(req).query, options)         : {};         console.log("parse(req).query:"+parse(req).query);         console.log("options:"+JSON.stringify(options));         console.log("qs.parse(parse(req).query,options):"+JSON.stringify(qs.parse(parse(req).query,options)));     }     console.log("req.query:"+JSON.stringify(req.query));      next();   }; };


运行结果为:

parse(req).query:_csrf=0MppzaqUwEvM1x6S+G8WFnMl96oQ99f+vVthM=options:undefinedqs.parse(parse(req).query,options):{"_csrf":"0MppzaqUwEvM1x6S G8WFnMl96oQ99f vVthM="}req.query:{"_csrf":"0MppzaqUwEvM1x6S G8WFnMl96oQ99f vVthM="}

最后发现,原来是一个叫qs模块(/express/node_modules/connect/node_modules/qs)的问题:

查看qu的源代码,由于传入的明显是string,所以应该是parseString(str)的问题:

进一步发现,这个方法内部调用了一个decode(str)方法:

function decode(str) {   try {     return decodeURIComponent(str.replace(/\+/g, ' '));   } catch (err) {     return str;   } }

终于发现了。。。奇怪,他为什么要换掉+号?难道是避免碰到拼接字符串?


解决方法:

我采取的方法是修改saltedToken(secret)方法:

function saltedToken(secret) {     var token = createToken(generateSalt(10), secret);     var re = new RegExp("\\+",["i"]);     var m = re.exec(token);     while(m!=null){         token = createToken(generateSalt(10), secret);         m = re.exec(token);     }      return token;     //return createToken(generateSalt(10), secret); } 

注:如果在客户端把+号改为空格,通过get方式也会变成%字符,而且这个decode方法也不能屏蔽,毕竟这还是很危险的。

原创粉丝点击