Bcrypt 的 72 字节陷阱

工具相关 ·

给用户表加上 Bcrypt 之后,安全性看起来提升了一个档次。但如果密码字段没有做长度限制,系统里就埋着一个不易察觉的问题:用户输入超过 72 字节的密码,超出部分会被算法静默忽略。这意味着两个前 72 字节相同的密码,会被判定为同一个。

截断发生在哪里

Bcrypt 的设计里,输入密码的长度上限是 72 字节。这不是实现缺陷,而是算法本身的限制——它源自 Blowfish 密钥扩展时使用的固定长度缓冲区。

关键在于「静默」两个字。超出的部分不是被拒绝,也不是报错,而是直接被丢弃。调用方拿不到任何提示,用户也不知道自己的密码只有前一段起作用。

这就造成了一个奇怪的局面:用户以为自己设了一个 80 字符的强密码,实际上系统只记住了前 72 字节;用户后来输入一个只有前 72 字节相同、后面不同的密码,也能成功登录。

字节不是字符

比「72」这个数字更容易被忽略的是单位:72 是字节,不是字符。

在 UTF-8 编码下,一个汉字占 3 个字节。所以:

24 个汉字 = 72 字节 → 刚好达到上限
25 个汉字 = 75 字节 → 第 25 个字被忽略

也就是说,一个 30 个汉字的密码短语,实际上只有前 24 个字参与了计算。如果用 emoji,情况更严重——一个 emoji 占 4 个字节,18 个 emoji 就会触及上限。

这个陷阱在中文环境里尤其容易踩到,因为很多团队会用「一句中文古诗」「一段中文短语」作为密码示例,而这些示例很容易超过 24 个汉字。

有哪几种处理方式

第一种,也是最推荐的:在业务层限制密码最大长度。

在注册和修改密码的页面上明确提示「密码长度 8 到 64 个字符」,并在服务端也做同样的校验。这样用户永远碰不到 72 字节的边界,问题从源头消失。限制长度对安全性没有损失——64 个字符的密码空间已经足够大。

第二种是预哈希。先对密码做一次 SHA-256,把结果交给 Bcrypt。SHA-256 的输出是 32 字节,永远不会超过 72 字节的限制。

但这个方案有一个需要小心的细节:SHA-256 的输出是原始二进制字节,其中可能包含 0x00。而 Bcrypt 在处理输入时遇到 null 字节会截断——这等于把问题从一个截断换成了另一个截断。

所以预哈希必须配合编码。正确做法是先把 SHA-256 的结果做 Base64 或十六进制编码,再交给 Bcrypt。编码后的长度分别是 44 或 64 个字符,都在限制之内。

第三种是换算法。Argon2 没有 72 字节的限制,输入长度可以到很大。如果系统还在选型阶段,直接选 Argon2id 可以避开这个问题。

怎么检查系统里有没有踩坑

如果系统已经在用 Bcrypt,可以按这几步排查。

先看注册和改密的校验规则里有没有长度上限。如果没有,再确认前端和后端是否都做了限制——只在前端限制不算,因为请求可以绕过前端直接发送。

然后看存储的哈希串。如果某个账号的哈希是用超长密码生成的,从哈希串本身看不出来,需要实际测试:用一个超过 72 字节的密码注册,再用「只改第 73 字节之后内容」的密码登录,如果能登录成功,就确认存在截断。

排查之后如果发现问题,处理方式是加长度限制,并提示受影响的用户重新设置密码。因为被截断的部分从未参与过计算,无法通过技术手段恢复。

检查清单

  • Bcrypt 只使用密码的前 72 字节,超出部分静默忽略,不报错
  • 单位是字节不是字符,UTF-8 下一个汉字占 3 字节,24 个汉字即达上限
  • 最推荐的处理方式是在前后端都限制密码最大长度
  • 预哈希方案必须配合 Base64 或十六进制编码,避免 null 字节截断
  • 新项目可以直接选 Argon2id,它没有这个长度限制
  • 排查时用「改第 73 字节之后的内容」测试能否登录,能登录即存在截断
阅读 12