使用Math.Random到底有什么问题?

300 阅读1分钟

当你搜索'JS如何生成随机字符串' 会遇到什么?


基本上都是类似下面的这种答案,核心是均是利用 Math.random 函数来实现,这里举2个例子:

function getRandomString(stringLength = 32) {    
  var chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678",
    charsLength = chars.length,
    result = "";
  for (i = 0; i < stringLength; i++) result += chars.charAt(Math.floor(Math.random() * charsLength));
  return result;
}
function getRandomNumber(max,min){
  return Math.floor(Math.random()*(max-min+1))+min
}

这么写有什么问题吗?


Sonarqube 检测平台中,针对类似代码,检测出了如下安全隐患:

笔者进行机器翻译后得到如下结果:

其中提到的三个漏洞:

综上,我们可以得出结论: 伪随机算法是安全敏感的

笔者了解到,严格来说,计算机的随机算法,全部都是伪随机算法,只有真实的物理现象(掷骰子、抛硬币、电子元件的噪音等等)才是真随机算法。但是同样作为 伪随机数,强度也有差异,此处提到的Math.random就是一种较弱的伪随机算法,不均匀,且被预测的风险较大

应该怎么写


根据 Sonarqube 检测平台提供的解决方案,我们得到如下 客户端和服务端 的修改建议

Sonarqube 推荐的安全编码实践

  • 使用加密强度高的伪随机数生成器 (CSPRNG),例如crypto.getRandomValues().
  • 仅使用生成的随机值一次。
  • 您不应公开生成的随机值。如果必须存储它,请确保数据库或文件是安全的。

Sonarqube 推荐的解决方案

const crypto = window.crypto || window.msCrypto;
var array = new Uint32Array(1);
crypto.getRandomValues(array); // Compliant for security-sensitive use cases
const crypto = require('crypto');
const buf = crypto.randomBytes(1); // Compliant for security-sensitive use cases

封装与使用


import * as crypto from 'crypto';

export default (randomStringLength: number = 10): string => {
  if (!(randomStringLength && randomStringLength > 0)) randomStringLength = 10;
  return crypto
    .randomBytes(Math.ceil(randomStringLength / 2))
    .toString('hex')
    .slice(0, randomStringLength);
};
import * as crypto from 'crypto';
export default (min = 0, max = 1000): number => crypto.randomInt(min, max);

遗留问题与思考


整体而言,安全措施做多少都不为过,因为往往出现安全事故后,损失都是不可逆的。如果是从浏览器端而言, Math.random 有多么容易被伪造被覆写,这个想比完全不需要什么理论支撑了,伪造门槛非常低。

但如果是从算法层面的安全性而言,可能笔者目前调研的深度还不够,暂时没有发现非常明显的随机算法的短板:

笔者针对随机数算法,进行了100000~999999六位数的重复率单元测试: 在100、1000、1w、10w次测试下,两者基本都在10w次重复未通过(其实也合理,样本本身就在10w级别了)

笔者针对 随机字符算法,进行了 4位随机字符 的重复率单元测试: 在100、1000、1w、10w次测试下,crypto.randomBytes 由于均为小写字符,导致表现远低于Math.random 对大小写+数字的随机结果。严格来说,这里并没有比较出算法本身的差异性。

也欢迎大家在评论区讨论和指导 ~