JavaScript 中如何让 a !== b && md5(a) === md5(b) 成立
问题描述
有这样一段 Node.js 代码,用于校验用户输入的两个密码是否一致,代码如下:
const crypto = require('crypto');
function md5(text) {
return crypto.createHash('md5').update(text).digest('hex');
}
function checkPassword(password1, password2, salt = '') {
if (!password1 || !password2 || password1.length !== password2.length) return false;
return password1 !== password2 && md5(salt + password1) === md5(salt + password2);
}
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
app.use(bodyParser.json());
app.post('/checkPassword', (request, response) => {
const { password1, password2 } = request.body;
const salt = Math.random().toString();
if (checkPassword(password1, password2, salt)) {
response.send('success');
} else {
response.send('fail');
}
});
app.listen(8080, '0.0.0.0');
这段代码接收用户传递的两个参数 password1
和 password2
,判断非空且长度不相同后,再判断两个参数是否不同且 MD5
计算后的值是否相同,满足条件则返回 success
。
为了方便大家测试这段代码,我们将代码中 Node.js 的部分简化并放在了码上掘金中供在线运行: jcode
问题分析
对这段代码进行分析,核心判断逻辑在 checkPassword
中,且做了以下限制逻辑:
- 输入参数的值为空的判断,和参数长度的判断
- 判断使用的是全等比较,无法借助隐式类型转换
MD5
采用了Node.js
或第三方成熟库,几乎无逻辑缺陷MD5
加了盐值,通过碰撞解决的难度很大
从分析来看这段代码的逻辑已经十分完善,那么我们如何实现返回 success
呢?
解决方案
我们注意到 password1
和 password2
是来自于 body-parser
通过 JSON.parse
后的结果,因此值的类型可能是 null
、string
、object
、number
、boolean
以及 undefined
其中的一个,undefined
是来自于解构时不存在 password1
时的默认值,例如输入为 {}
。因此参数类型并不一定是 string
,类型这里似乎有漏洞。我们依次分析下:
- 参数类型为
string
,是预期情况,从上面的问题分析来看无解 - 参数类型为
null
或undefined
,不满足参数为空判断 - 参数类型为
number
或number
,无length
属性 - 参数类型为
object
,可行吗?
是的,object
是解决问题的途径,我们再次分析下 checkPassword
,看如何逐条破解其中的判断:
- 输入参数为空判断,
object
天生满足 length
必须相同,我们可以给两个object
参数添加相同的length
属性满足password1 !== password2
,对象是引用类型,两个对象引用不相等md5(salt + password1) === md5(salt + password2)
,加号运算会引发对象的类型转换,toString
后变成[object Object]
,因此MD5
运算两边的字符串是相同的
由此可见,当我们的输入参数为:
{
"password1": {
"length": 1
},
"password2": {
"length": 1
}
}
即可返回 success
。
进一步考虑,数组就是有 length
的对象,因此也可以是:
{
"password1": [],
"password2": []
}
总结
通过对这个问题的分析,我们找到了问题的答案,关键点在于输入参数的类型上。我们知道了,全等比较并不是万能的,还需要对参数的类型做严格判断。否则 string
+ object
会引发对象的隐式转换,导致了 MD5
值相同,绕过了我们的判断逻辑。
希望大家以后在开发类似功能时可以避免这个问题。