![]()
2026年3月,Node.js安全团队处理了一个编号CVE-2026-21717的漏洞。修复方案很反常:他们设计了一种哈希算法,既要让攻击者算不出来,又要让系统自己能瞬间还原。这听起来像让一把锁同时做到"小偷打不开"和"主人不用钥匙"——但V8团队真做出来了。
HashDoS:从浏览器无害到服务器致命
哈希表是软件的基石。理想情况下,插入和查找都是O(1)操作。但攻击者如果构造大量碰撞键,能把复杂度逼到O(n²)。在浏览器里,这顶多卡死一个标签页,用户关掉就行。Node.js不一样:单线程事件循环一旦被堵,整个服务就挂了。
V8的字符串哈希原本用确定性算法,速度快但可预测。安全研究员向Node.js团队报告了这个攻击面后,问题变得棘手——加随机化能防HashDoS,但会打碎V8依赖哈希值的性能优化。
具体说,V8内部用字符串的哈希值做快速相等性检查。两个字符串指针不同,先比哈希值,只有哈希撞了才逐字符比对。这个优化省了大量CPU周期,前提是哈希值能随时取到。如果换成标准密码学哈希(如SHA-256),每次算哈希的成本会吃掉所有收益。
团队需要一个"单向对用户、双向对系统"的方案。
设计矛盾:不可预测 vs 可逆
![]()
传统解决HashDoS的路子有两条。一是加秘密随机种子,让攻击者无法离线构造碰撞键——但V8的字符串哈希在编译期就确定了,改不了。二是用密码学哈希或伪随机函数(PRF),输出完全打乱,可这也意味着系统自己也无法从哈希值反推原值。
V8的优化恰恰需要反推。某些场景下,运行时要把哈希值映射回原始整数,用于去重或缓存命中判断。如果哈希是单向的,这些代码路径要么重写,要么放弃优化。
团队最终选择了一个折中:设计一个基于秘密密钥的整数排列(permutation),满足三个条件——攻击者不知道密钥就无法预测输出;知道密钥就能高效逆运算;正向计算足够快,不拖慢正常操作。
这本质上是一个带密钥的轻量级分组密码,但块大小是32位或64位整数,而非标准AES的128位。更小的块意味着更少的轮数,更快的速度,但也更容易被暴力分析。
具体实现:四步混合
算法核心是一组按位操作,混合输入整数与两个秘密密钥(k1, k2)。流程大致是:先用k1做一次乘法-异或(xorshift风格),中间结果再与k2混合,最后再来一轮乘法-异或。每一步都是可逆的,逆运算只需用模逆元倒推。
乘法选的是奇常数,保证在2³²或2⁶⁴模空间内有逆元。异或操作自逆,乘法逆元用扩展欧几里得算法预计算。整个正向计算只需几条CPU指令,逆运算也多不了几条——比AES-NI指令集还轻量。
![]()
密钥k1、k2在V8实例启动时随机生成,从不暴露给JavaScript层。攻击者即便能观测哈希输出,也拿不到密钥,无法构造碰撞输入。
团队用SMHasher测试套件跑了统计检验。 avalanche效应、位独立性、碰撞分布都达标,但没有达到密码学PRF的强度——他们故意留了这个缺口。更强的混淆意味着更多轮数、更慢速度,而威胁模型里攻击者是"盲"的:看不到密钥,也无法通过时序侧信道批量探测。这个设计足够让盲攻击者失效,同时保住性能。
性能账怎么算
新哈希的额外开销主要来自乘法指令。现代CPU上,64位乘法延迟约3-4周期,吞吐每周期1条。对比原本的单条哈希指令,理论 slowdown 在2-3倍。但V8的字符串哈希不是热点——实际跑分显示,典型Node.js工作负载下整体性能损失低于0.5%。
逆运算路径更少触发,成本可以忽略。团队重点验证了去重缓存和隐藏类(hidden class)转换场景,确认没有回归。
这个方案2026年3月随Node.js安全版本发布。同月,V8上游也合并了相关补丁。浏览器端的Chromium没动——那里的威胁模型不同,DoS不算安全漏洞。
代码已经开源。如果你维护一个需要防HashDoS、又依赖哈希值做优化的系统,这套"可逆的不可预测性"设计或许能抄作业——但得先想清楚:你的攻击者真的是"盲"的吗?
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.