hash_equals是强制安全要求,用于签名、令牌、密码哈希等场景,防止时序攻击;必须将服务端已知字符串放第一参数、用户输入放第二参数,且需预先校验长度一致,否则可能泄露密钥长度信息。

hash_equals 不是“可选优化”,而是涉及签名、令牌、密码哈希比对等场景时的**强制安全要求**。用 === 直接比较用户输入和密钥派生值,等于把校验逻辑的执行时间暴露给攻击者。
为什么必须把用户输入放在第二个参数?
hash_equals 的设计逻辑是:第一个参数是服务端已知的、长度固定的密钥派生值(比如 HMAC 输出),第二个参数是不可信的、可能被操控的用户输入。
如果调换顺序,函数仍能运行,但违背了安全假设——它无法保证在长度不匹配时隐藏 $known_string 的真实长度信息。攻击者可通过反复提交不同长度的伪造签名,观察响应延迟或返回 false 的时机,反推出密钥派生结果的长度,进而缩小暴力破解范围。
常见错误写法:if (hash_equals($_GET['sig'], hash_hmac('sha256', $data, $key))) { ... }
这会让攻击者控制第一个参数长度,直接破坏防护前提。
字符串长度不一致会立刻返回 false,这安全吗?
会立即返回false 是设计行为,不是 bug,但需要你主动规避风险:
- 所有参与
hash_equals比较的字符串,必须提前确保长度一致; - HMAC 输出长度由算法决定(如
sha256固定 64 字符十六进制,或 32 字节二进制),不要混用编码格式; - 用户传来的签名若长度不对,应直接拒绝,不进入
hash_equals—— 否则等于泄露了正确签名的长度; - 避免对用户输入做
base64_decode或hex2bin后再比对,除非你确认两边解码后字节长度严格一致,且解码失败时抛出异常而非静默截断。
哪些场景必须用 hash_equals,哪些可以不用?
必须用:- 验证 URL 中的 HMAC 签名(如
$_GET['sig']); - 比对 session 中存储的 CSRF token 和表单提交的 token;
- 校验 password_hash() 生成的哈希与用户输入密码经 hash 之后的结果(注意:实际应优先用
password_verify(),它内部已用恒定时间比较); - 比对 API key 或 webhook secret 的哈希摘要。
- 普通表单字段校验(如用户名、邮箱格式);
- 数据库查询结果的非敏感字段匹配;
- 任何不涉及“秘密已知值 vs 不可信输入”对比的场景。
PHP 版本低于 5.6 怎么办?
hash_equals 自 PHP 5.6 起内置,老版本需手动兼容。但别抄网上那些看似简洁的 XOR 实现(如 substr_count($a ^ $b, "\0") === strlen($a)),它们在空字符串、多字节字符、或存在 \0 字节时行为不可靠。
推荐补丁(来自 PHP 官方文档用户注释,经多年验证):if (!function_exists('hash_equals')) { function hash_equals($a, $b) { $ret = strlen($a) ^ strlen($b); $ret |= array_sum(unpack('C*', $a ^ $b)); return $ret === 0; } }
null 或数组——这些类型错误会触发 E_WARNING,可能暴露路径或引发意外流程跳转。
真正容易被忽略的点在于:你得确保整个验证链路里,从接收、解析、到比对,每一步都维持恒定时间语义。哪怕只在一个地方用了 ===,整条防线就垮了。



















