Cr4zy Uru5 blogs
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');

这段代码接收用户传递的两个参数 password1password2,判断非空且长度不相同后,再判断两个参数是否不同且 MD5 计算后的值是否相同,满足条件则返回 success

为了方便大家测试这段代码,我们将代码中 Node.js 的部分简化并放在了码上掘金中供在线运行: jcode

问题分析

对这段代码进行分析,核心判断逻辑在 checkPassword 中,且做了以下限制逻辑:

  • 输入参数的值为空的判断,和参数长度的判断
  • 判断使用的是全等比较,无法借助隐式类型转换
  • MD5 采用了 Node.js 或第三方成熟库,几乎无逻辑缺陷
  • MD5 加了盐值,通过碰撞解决的难度很大

从分析来看这段代码的逻辑已经十分完善,那么我们如何实现返回 success 呢?

解决方案

我们注意到 password1password2 是来自于 body-parser 通过 JSON.parse 后的结果,因此值的类型可能是 nullstringobjectnumberboolean 以及 undefined 其中的一个,undefined 是来自于解构时不存在 password1 时的默认值,例如输入为 {}。因此参数类型并不一定是 string,类型这里似乎有漏洞。我们依次分析下:

  • 参数类型为 string,是预期情况,从上面的问题分析来看无解
  • 参数类型为 nullundefined,不满足参数为空判断
  • 参数类型为 numbernumber,无 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 值相同,绕过了我们的判断逻辑。

希望大家以后在开发类似功能时可以避免这个问题。