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

背景:

在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-DgylWwCYUb
createToken(token.slice(0,10),secret):RLaNpX4nMPvsv2+gjZ+Rhe911/dDo0VA4Sa4A=
false
再次测试,正常为:

token:dyqRPL8L0ikahAeEMP2Wr/cNcD/hjPRUV0KlM=
secret:BHNfrSL2QRTiK-DgylWwCYUb
createToken(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:undefined
query._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:undefined
qs.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方法也不能屏蔽,毕竟这还是很危险的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值