摘要

  • AFRINIC 当前 WHOIS Crypt 页面的 HTML 声明了一个 POST 表单:密码标签所对应的控件是 type="text",提交字段名则是 plaintextpassword,相对地址指向同一个 AFRINIC 域名。本次调查没有输入任何密码,也没有提交表单。
  • 生成出的 BCRYPT 字符串只能说明输出是什么,不能说明输入在生成时由谁保管。AFRINIC 的成员指南又写明,只有哈希值应当与 AFRINIC 分享,明文密码应由成员安全保存,因此中间这一步需要一份可核验的说明。

一串哈希值很容易把视线引向终点。它看起来像证据:盐值在其中,算法标记在其中,最终可放入 WHOIS 对象的 auth: 属性中。可一个控制体系最容易失去的,恰恰是哈希产生之前的那几步。密码在哪台机器上存在过?浏览器把它交给了谁?脚本是否有机会接触?服务器是否把请求体写入日志?这些问题不会被编码进最终的 BCRYPT 字符串。

AFRINIC 的公开路径并不隐蔽。WHOIS utilities 页面介绍了用于产生 BCRYPT-PW 值的工具,并把读者引向独立的 WHOIS Crypt 应用。2026 年 9 月 14 日保存的原始 HTML 给出了足够清楚的声明。表单标识为 myForm,方法为 POST,动作地址是相对路径 ?lang=en#cli。一个写着“Password”的标签指向输入控件。该控件不是 HTML 的密码类型,而是 type="text";它的 id 与提交时使用的 name 都叫 plaintextpassword。按钮文字是“Generate hash”。

这些是文档事实,不是一次在线实验。本次调查没有把真实密码、测试密码或任何成员凭据填入控件,没有按下按钮,没有截取网络流量,没有调用服务器端接口,也没有创建或修改 WHOIS 对象。我们看到的是浏览器被要求执行的动作,而不是某次动作完成后的观测结果。

HTML 标准对这类声明的含义并不含糊。表单由用户可填写的控件组成,数据可以被发送到服务器进一步处理;标准也以 HTTP POST 请求体承载参数作为常见的服务器处理方式。因为 Crypt 页面使用相对动作地址,所以在通常的表单语义下,提交目标解析为当前的 whois-web.afrinic.net 源。字段名又直接把待提交的应用值标为 plaintextpassword。因此,页面声明的是“把哈希前的密码交给 AFRINIC 接收端进行处理”。它并未声明“只在本机计算”。

这里必须立刻放上最重要的反证:页面使用 HTTPS。TLS 的目标包括防止传输过程中的窃听与篡改。没有任何已保存材料支持“密码以可读明文在网络上裸奔”这种说法。本文所称的明文,是相对于哈希而言的应用层原始值,不是相对于 TLS 而言的未加密网络载荷。加密隧道可以很好地保护一段远程提交;它不能把远程接收重新定义为本地计算。

接收也不等于留存。服务器完全可能只在内存中短暂取得输入,计算 BCRYPT 后立即释放,并且从所有日志、错误与追踪系统中排除该字段。相反,一个通用代理、异常记录器或可观测性组件也可能在开发者没有特别配置时复制请求内容。我们没有服务器代码、代理配置、日志样本或运行时追踪,因而不能在这两种情形中作出选择。证据的边界应当停在“存在公开声明的接收路径”,而不能跳成“发生泄露”。

AFRINIC 自己的成员指南把这个边界变得值得追问。指南在讲解 maintainer 对象时,建议成员为明文密码生成 BCRYPT 哈希;紧接着,它写明只有哈希值应当与 AFRINIC 分享,明文密码应由成员安全保存。最宽厚的解释是,这句话只限定最终进入 WHOIS 数据库的内容:auth: 中放哈希,不放原始密码。按这种理解,一个远程生成器并非必然与指南冲突,因为服务器可以临时接收后只返回哈希。

然而,“只分享哈希”在普通读者眼中也可能意味着另一件事:原始密码不离开成员设备。当前工具没有告诉用户,它采用的是哪一种含义。指南描述了最终状态,工具声明了中间流向,两者之间缺少一句可验证的衔接。这不是法律违规的证据,不是隐私事故的证据,也不是 AFRINIC 作出虚假承诺的证据;它是一项范围尚未定义的承诺。

输入控件的类型则暴露了另一个较小、但具体的决定。HTML 中,type="password" 是用于敏感文本并默认遮蔽输入的状态;type="text" 是普通文本控件。保存的页面使用后者。这并不证明有人从肩后看到了密码,也不说明 BCRYPT 参数薄弱,更不等于数据已经外泄。它只说明页面没有请求浏览器采用专门的密码遮蔽控件。屏幕可见性、网络传输、端点处理、日志留存是四个不同控制,不能用修复其中一个来代替其余三个。

页面还把浏览器脚本带入了边界。HTML 从 challenges.cloudflare.com 加载 Cloudflare Turnstile,同时加载本地路径下的 jQuery、Mustache 与 main.js。OWASP 对第三方 JavaScript 的指导之所以强调变更控制和敏感数据,是因为运行在页面上下文中的代码原则上可能接触文档里的数据,而第三方交付的代码又可独立变化。这是风险模型,不是事件报告。仅仅看到 Turnstile 的 <script> 标签,不能证明 Cloudflare 读取、接收、保存或外传了密码。

对链接的本地 main.js 做静态检查,可以排除一项同样不负责任的推断。在保存的版本中,没有 myForm、plaintextpassword、BCRYPT 或 Crypt 生成器的表单钩子。该文件会在找到另一个名为 create-container 的界面时初始化视图,处理 WHOIS 对象模板的载入、属性编辑与保存,并在那套对象编辑流程里使用另一个密码字段。它不是 Crypt 页面在浏览器端哈希的证据。

但一份脚本的静态缺失也不能承担过多结论。它不能证明 jQuery、Mustache、Turnstile、未来脚本、浏览器扩展或动态响应永远不会接触控件;更不能说明服务器在 POST 之后做什么。可靠的说法只能是:这份已链接的本地脚本中未见对 Crypt 表单的本地哈希处理,而页面仍声明普通 POST。其余行为未知。

AFRINIC 的当前隐私政策提供了实质性的正面背景。政策把 AFRINIC 定义为数据控制者,列出包括 WHOIS 服务在内的信息收集来源,限制指定人员访问,要求共享数据的第三方承担保护义务,并把保存期限与组织目的及法律要求相连。这些承诺不应被忽略。问题在于,它们是覆盖多类服务的总政策,并没有为 plaintextpassword 这一个字段提供处理凭证。

用户仍然不知道请求体是否进入反向代理日志,应用是否在异常时记录参数,可观测性平台是否采样表单内容,支持人员的诊断包是否会包含输入,内存中的明文生命周期有多长,以及 Turnstile 是否与敏感控件做了隔离。总政策可以规定责任原则,却不能自动证明每一条技术链路满足原则。

日志是最适合把抽象问题变成测试的地方。OWASP 的日志指导把认证密码列为通常不应直接记录的数据;如果相关事件必须记录,应当删除、遮蔽、清洗、哈希或加密敏感内容。我们没有查看 AFRINIC 的任何日志,因而不会声称它记录了密码。相反,这条指导让公开字段名成为可验证目标:plaintextpassword 的值不应出现在应用日志、代理日志、分布式追踪、分析事件、错误报告或支持快照中。

“使用 HTTPS”无法替代这个测试。“输出是 BCRYPT”也无法替代。TLS 约束传输链路上的观察者;BCRYPT 约束最终验证材料面对离线猜测的一部分风险;日志排除约束端点周围的复制;脚本隔离约束浏览器文档内的权限。它们不是竞争答案,而是不同层次的问题。

一个中心化服务器生成器并非天然错误。它可能降低成员配置门槛,减少格式错误,帮助用户迁离过时方法。真正的问责要求是把便利所引入的保管步骤说清楚。如果服务端计算是设计选择,页面可以明确写出:接收端通过 HTTPS 接收一个全新且不复用的密码,只为计算哈希而短暂处理,不记录、不追踪、不保留明文,并限制任何第三方处理者的访问。随后用合成值在授权环境中持续验证这些性质。

另一条路线是在浏览器本地生成哈希。那同样不能靠一句“本地计算”就获得信任。实现需要可审阅代码、固定依赖、明确的完整性策略,并证明敏感控件与第三方脚本的能力分离;还要证实任何出站请求只包含允许的非敏感数据。架构名称不是凭证,运行边界才是。

单独把 type="text" 改成 type="password" 值得做,却只能修复视觉输入层。单独增加隐私政策链接也不够,因为总则不能回答字段级日志问题。单独展示锁形图标更不能说明服务器保留多久。真正完整的页面应当让成员在输入前就知道:哈希在哪里生成、谁是接收者、哪些系统被禁止保存、失败路径怎样处理、第三方脚本能否访问。

失败路径尤其容易被正常结果遮住。一次成功计算也许只返回哈希,但超时、输入过长、挑战验证失败、应用异常或代理拒绝时,通用错误组件可能接管请求。许多系统会为了排障提高日志详细度,正是在这种时刻,原本不需要保存的字段最容易进入异常上下文。本文没有观察到任何一种情形,也没有证据说 AFRINIC 的系统存在这种行为;可一份真正完整的凭证必须同时覆盖成功与失败,而不能用正常页面的一次正确输出代表全部路径。

同样需要区分“算法输出正确”与“请求身份正确”。即使两个实现对同一合成输入产生可接受的 BCRYPT 格式,也不能由此推出请求没有被分析系统复制、浏览器脚本没有额外权限、服务端没有重试缓存,或者支持人员无法读取诊断数据。比对哈希只验证了计算的一部分。控制凭证还应列出经过的组件、每个组件能看到的字段、字段被过滤的时点以及删除是否可复核。

工具的使用语境也值得被写进说明。成员指南说应由成员安全保管原始密码,这意味着用户很可能把这个页面理解为一种受信任的辅助计算器,而不是传统登录框。登录服务接收密码通常是功能本身的一部分;生成器却可能选择本地或远程两种模式。正因为两种架构都可行,界面更有义务在输入之前告诉用户当前选择。不能把用户对“生成工具”的直觉当成同意远程处理,也不能把服务器处理本身描绘成必然不当。

一份最小而有用的公开说明不需要暴露防御细节。它可以只列出五个事实:计算位于浏览器还是服务器;表单值是否离开设备;服务端是否明确禁止请求体日志;明文是否在响应完成前销毁;第三方脚本是否能读取敏感控件。随后给出版本或更新时间,使成员知道承诺对应哪一版工具。对安全团队而言,这五项也是变更审查清单:修改表单、验证码、代理、错误处理或分析平台时,都要重新确认原有边界。

这类记录还有一个制度价值。它让隐私政策、成员指南与工程实现不再各自正确却彼此脱节。法律文本可以规定目的与责任,指南可以告诉成员怎样管理对象,工程凭证则说明某个字段在某一条实际路径上的生命周期。三个层面互相引用,才足以让“只分享哈希”成为一致的服务承诺,而不是需要读者自行补全的语义。

还应当把“保证”与“观测”分开。机构写下“不记录密码”是一项保证;在授权环境里用合成标记逐一检查日志、追踪、错误与支持材料,并确认没有命中,是一次观测。保证给出责任,观测给出特定版本上的证据。两者结合仍不等于对未来所有版本作永久证明,却比一串无法说明来路的哈希更能支撑成员判断。

凭证也应记录未覆盖项。例如,若测试只走了正常响应,就必须明确没有覆盖超时与异常;若只检查应用日志,就不能把结论延伸到代理或外部分析;若只静态审阅当前 main.js,就不能替其他脚本作担保。把空白写出来不是削弱结论,而是阻止结论在传播中膨胀。

最后,任何改变都应触发有范围的重验。表单字段、Turnstile、反向代理、错误收集器和分析平台可以独立更新。旧结果不能自动认证新组合。使用同一套合成值规则、组件清单与失败分支检查,就能让每次变更只重验受影响的环节,而不必用模糊的“系统整体安全”替代具体证据。这种有版本、有日期、有限定的记录,才符合工具所承担的信任重量。

成员看到的最终哈希还应与这张凭证保持逻辑分工:哈希证明计算产物,凭证证明输入路径。前者可以被复制进对象,后者应由服务运营者持续维护。只有把两者同时交付,用户才不必从一个正确的输出反推一条未经证明的保管链。

资料来源