背景:
在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方法也不能屏蔽,毕竟这还是很危险的。