本地弱口令名单能证明什么,为什么它不是全网泄露查询

检测页弹出「未命中弱口令名单」,很多人会读成「这个密码从没出现在任何一次拖库里」。这两句话差得很远。一份随页面下载的高频名单,只能回答「像不像已经被用烂的口令」;Have I Been Pwned 一类服务回答的是另一件事,而且查询方式也不同。下面把能写进结论的句子收窄,并给出可以当场核对的步骤。

「未命中」不是「从未泄露」

弱口令检测给出的结果通常只有两种:命中,或未命中。命中好理解——这串口令已经出现在某份公开的高频名单里,继续用它等于把自己放进攻击者最先试的那一批。未命中则常被多读一层:既然名单来自「泄露」,没中就是「干净」。

名单只能对它收录过的条目负责。一次未公开的内部泄露、一份还没被整理进公开集合的 infostealer 日志、你在某个小站点上单独被拖走的口令,都不会自动出现在浏览器刚下载的那几百或几千行文本里。未命中的完整读法是:在当前这份本地名单、当前这次比对规则下,没有找到相同或允许的变形。它没有对「全网」做过承诺。

把这句话写清楚,是为了避免下一步做错。命中时,更换是对的。未命中时,仍然要看长度、是否复用、是否来自密码管理器随机抽出。名单是过滤器,不是无罪证明。

名单防的是哪一类猜测

攻击者很少从 a 穷举到 zzzz…。更常见的两条路径是:拿已经泄露的用户名和口令去别的站试(撞库),以及按公开弱口令排行从前往后试(在线猜测)。123456passwordqwerty 会反复出现在这些名单的最前面,不是因为它们「看起来短」,而是因为它们被选得足够频繁,试几次就能碰到。

高频名单针对的就是这一层:把已经被用滥的口令标出来,让你在自己还能改的时候丢掉它们。它不回答「这串字符的搜索空间有多大」,也不回答「某个尚未公开的库里有没有这一条」。后两件事分别属于熵估算和规模大得多的泄露语料,不该和「像不像常见弱口令」绑成同一个结论。

这也解释了为什么一份并不算长的名单仍然有用。在线登录几乎都会限流。攻击者在被锁之前能试的次数有限,他们会优先试排行靠前的口令。NIST SP 800-63B-4 在 blocklist 一节写过:名单的目的是挡住在限流耗尽前就很可能被猜中的口令;名单大到超过这个目标之后,增量收益很小,因为在线攻击本身已经被节流。本地高频名单和这条目标是对齐的,它本来就不是按「收录宇宙中每一条曾泄露的口令」来设计的。

NIST 要求对照整串,而不是再加一套复杂度规则

美国国家标准与技术研究院在 SP 800-63B-4 里,把「对照已知常用、可预期或已泄露的口令」写成验证方必须做的步骤:建立或修改密码时,应当把整串候选口令拿去和 blocklist 比较,而不是看它中间有没有某个单词、是不是缺了符号。命中则必须拒绝,并告诉用户为什么被拒。

同一节还改写了过去十几年里最容易被误解的口令政策。验证方不得再强制「必须同时有大写、数字和符号」这类组合规则;不得要求定期改密码,除非有证据表明这枚认证器已经泄露;单因素口令长度下限写成 15 个字符,作为多因素之一时下限是 8。这些句子针对的是要接收口令的网站,不是浏览器里的自测页。但对阅读检测结果的人,它们仍然有用:复杂度勾选本身证明不了安全;真正该先挡掉的,是已经出现在公开名单里的整串。

NIST 还提醒:拒绝之后要给出换口令的指引,避免用户只做「password 改成 Password1」这种一眼能看穿的改写。本地名单如果额外处理大小写、尾部两三位数字、常见 Leet(@ 当 a、0 当 o),针对的就是这类改写。它仍然是启发式,不是密码学证明:P@ssw0rd2026! 可能被抓住,换一种没写进规则的变形就不会。

不要用正在使用的真实口令做公开实验。需要对照名单时,用已经决定废弃的旧口令,或一段可以丢弃的测试串。核对的是规则和流量,不是把现用口令再暴露一遍。

本地短名单、哈希前缀查询、全量离线库

「查过泄露没有」至少有三种做法,混在一起就会把未命中读成从未泄露。差别不在口号,在数据走了哪条路、服务器能看见什么。

第一种是本地短名单。页面把一份公开文本下载到当前标签页,脚本在内存里做成集合,再拿你输入的整串(以及少量变形)去查。出站的是名单文件本身,不是待测密码。覆盖面等于这份文本有多少行。以 MakePwd 随页提供的 leaked-top10k.txt 为例,文件名写着 top10k,打开后可以按行数自行清点——当前是八百余条高频条目,不是一万条,更不是全网。名单加载失败时,实现会退到内置的最小集合(例如 password123456),覆盖面更窄,检测仍留在本机。

第二种是 Have I Been Pwned 的 Pwned Passwords 范围查询。官方文档把语料写成「数以亿计、曾出现在泄露中的口令」。客户端先在本地做 SHA-1(或 NTLM),只把哈希的前 5 个十六进制字符发给 https://api.pwnedpasswords.com/range/{prefix},再在本地把返回的后缀和自己算出的完整哈希比对。明文和完整哈希都不会按设计发给服务端。这是 k-anonymity:服务器只知道你落在约一百万个前缀桶里的哪一个。它仍然是查询外部接口,只是查询的不是口令原文。2025 年 11 月,HIBP 曾一次性纳入约 13 亿条不重复口令(其中约 6.25 亿条对该服务是新的);2026 年 6 月又从 stealer 日志中加入约 1.24 亿条不重复口令。这些数字说明的是语料在膨胀,不是「查过一次就永远干净」。

第三种是把整份语料下载到自己的机器或内网,完全离线比对。HIBP 也提供这种下载。覆盖面接近第二种,但不再向 api.pwnedpasswords.com 发送前缀。代价是更新、存储和你自己维护哈希索引。

做法 离开本机的数据 未命中能写成
本地高频名单 名单文件;不应有待测密码 不在这份名单(及已实现的变形)里
HIBP 范围查询 SHA-1 / NTLM 的 5 位前缀 不在当前 Pwned Passwords 语料里
全量离线库 无查询请求 不在你下载的那一版语料里

三种说法都不能改写成「从未出现在任何一次泄露中」。未公开的库、尚未被收录的日志、你从未交给任何第三方的口令,都不在这些集合的义务范围内。本地名单只是其中最窄的一种,优点是待测密码按设计不必离开标签页。

命中和未命中,各自只能写进哪句结论

命中时,结论可以写得很硬:这串口令已经出现在公开高频名单里,或只做了名单能够识别的改写。继续用它,等于把自己留在撞库和在线猜测最先碰到的集合里。正确动作是换掉,并且不要在其他站点复用同一串。

未命中时,结论必须带范围。它支持「不像这份名单里的常见弱口令」;不支持「没被拖过库」「可以在所有站点复用」「熵足够高」。一个 20 位随机口令会未命中,一个只在某次未公开泄露里出现过的口令也会未命中。结果看起来一样,含义不同。

还有一种「看起来很长、仍然该标弱」的情况。名单命中优先于熵:password 只有 8 个字母,按字符集估算会得到一个并不为零的比特数,但任何人都会先试它。本地检测若在命中后把等级改成弱,依据的是名单,不是那串比特数算错了。反过来,未命中加上很高的熵,也只说明「不像常见弱口令,并且穷举成本看起来高」,不能改成「已经安全」。复用、钓鱼、对方站点存明文,都不在这次比对的范围内。

写给同事的最短可用句子:命中 = 立刻换掉;未命中 = 只排除了这份名单,下一步仍看是否复用、是否随机生成。

当场核对:看名单文件,再看 Network

口号写「本地比对、不上传」无法自证。能当场看到的是两样东西:浏览器下载了哪份名单,以及你输入的那串有没有作为业务数据出站。

先看名单。打开开发者工具的 Network,过滤文件名里带 leakedtop 的请求,把响应另存为文本。用编辑器数一下非空行,抽查前几行是不是 123456password 这类公开高频口令。你核过的行数,才是「未命中」这句话的分母。不要凭文件名里的 10k 直接写成一万条。

再看待测密码。准备一枚不会出现在真实业务里的金丝雀,例如 canary-audit-2026-x7k,输入检测框。勾选 Preserve log,在过滤器里粘贴这串。XHR / Fetch 的请求行、查询串和请求体都不该出现它;访问统计的 query 与 body 同样不该出现。静态资源、字体和那份名单文本可以出现——名单出站是预期,待测密码出站才是失败。

  1. 打开 Network,勾选 Preserve log,先完成一次检测,再找名单文件的响应并清点行数。
  2. 换一枚金丝雀口令再测一次,用过滤器搜索这枚字符串;命中任意请求即说明明文离开了标签页。
  3. 没有命中时,再打开访问统计请求,确认标题和路径可以出现,输入框全文不能出现。

这套步骤证明的范围很窄:这一次操作里,待测密码没有作为已观察到的 HTTP 字段离开当前标签页,名单覆盖面等于你数过的行数。它不证明扩展程序没有读输入框,也不证明下周更新后的脚本仍是同一行为。换浏览器或换版本后,值得再做一遍金丝雀搜索。

熵估算和名单是两件不同的事

检测页往往同时给出等级、比特数和「是否命中名单」。把它们当成同一个分数,就会出现「比特数不低、却命中名单」或「未命中、却很短」时的困惑。它们回答的问题不同,也不该互相抵消。

熵估算问的是:假如攻击者按字符集穷举,平均要试多久。常见写法是长度乘以 log2(字符集大小),再对重复字符、连续重复做折扣。阈值如果写成 ≥ 80 bits 为极强、≥ 60 为强、≥ 40 为中,其余为弱,那只是这条估算函数的分档,不是实验室测出的真实破解时间。离线与在线两条时间线用的猜测速率也不同:例如离线按每秒 1010 次、在线按每秒 103 次,再取平均情况(搜索空间的一半)。换一套速率,时间会变,口令本身没有变。

名单问的是:这串是不是已经被公开滥用。它不使用比特数。一个命中名单的口令,即使碰巧混进了大写和符号,仍然该标弱。一个未命中的 6 位口令,熵会很低,长度本身就会把它压成弱——这和名单无关。低于 8 位时给出「安全性较低」的提示,依据的是长度,不是泄露库。

所以一次完整阅读至少看三处:名单是否命中,长度是否过短,熵落在哪一档。只有三处都读过,才谈得上「这一串现在看起来像什么」。它仍然代替不了密码管理器,也代替不了「每个站点一串随机口令」。

用打开即用的检测页把范围练熟

若你希望用一个把边界写清楚的页面来练习,可以从 MakePwd 的密码检测开始。打开即可使用,无需注册,也没有密码库。待测密码按产品说明不上传,也不会写入 analytics;比对发生在当前标签页下载的公开高频名单上,不是 Have I Been Pwned,也不是全网撞库。命中会强制标为弱;超过 128 位时只做精确匹配,不再跑尾部数字和 Leet。

练习时用可丢弃的测试串,不要用现用口令。同时打开 Network:你应看到名单文件和可能的访问统计,不应看到输入框全文。新口令需要另抽一串时,密码生成器同样打开即用,随机模式长度 6–128 位,默认 16 位,低于 8 位会提示安全性较低。生成和检测都在浏览器里完成,明文默认不离开本机。

这些练习的目的不是证明某一个站点「查得最全」,而是让你能向同事解释三句话:我核过名单有多少行;我用金丝雀搜过 Network;未命中我只写成「不在这份名单里」。换任何声称「本地弱口令检测」的页面,步骤可以原样搬过去。

下次读检测结果时记住的三件事

第一,命中就换;未命中只排除了当前这份名单,不要读成从未泄露。第二,本地短名单、HIBP 的 5 位哈希前缀、全量离线库是三种不同的数据路径,未命中的含义跟着路径走。第三,熵和名单不要互相抵消——短口令可以未命中,长而常见的口令可以命中。

若你还要继续追问「明文有没有作为业务数据离开这个标签页」,请读浏览器里做加密,怎么当场核对明文没有离开本机。本文只把「弱口令名单覆盖什么」收成可以写进结论的范围。