RamenHire 求职板中的一个存储型 HTML 注入漏洞,允许任何人填写公开表单,并将团队的通知邮件变成一个网络钓鱼平台。
漏洞是如何产生的
RamenHire 基于 Next.js 和 Supabase 构建,允许初创公司无需登录即可发布职位。每次提交都会向内部招聘团队发送一封电子邮件。邮件正文是通过将原始表单字段(公司名称、职位描述、联系人姓名)直接拼接成 HTML 字符串构建的。在字符串到达邮件服务之前,没有进行任何转义或净化处理。
由于模板将用户输入视为安全的 HTML,恶意申请人可以注入一个虚假的“点击此处进行验证”链接。该有效载荷(payload)作为职位列表的一部分存储在服务器上,因此后续发送的每一封通知邮件都会携带恶意代码。攻击者可以将该链接隐藏在真实的“查看仪表板”按钮旁边,从而诱骗团队成员泄露凭据或访问钓鱼网站。
为什么这很重要
这些邮件会发送给核心招聘团队,他们为了推进候选人流程而需要频繁点击链接。
技术缺陷
HTML 转义并非单一步骤。有两个上下文需要保护:
- 文本节点 (Text nodes) – 标签之间的内容。将
<转换为<以及将>转换为>可以阻止标签的生成,但无法阻止攻击者跳出属性值。 - 属性值 (Attribute values) – 标签内部的字符串,例如
href="…"。注入引号("或')可以让攻击者提前闭合属性,并插入自己的标记。
原始代码仅处理纯文本,导致属性注入漏洞处于完全开放状态。
修复问题
开发人员添加了一个小的 escapeHtml 辅助函数,并在插值之前将其应用于每个用户提供的数值,无论该值最终是出现在文本节点还是属性中。
验证步骤
- 本地测试 – 将一系列注入字符串输入模板函数。每次测试都仅显示转义后的字符,没有可执行的标签。
- 生产环境测试 – 部署补丁后,通过正式网站提交了一个有效载荷,并成功通过了 Cloudflare Turnstile 的机器人检查。生成的邮件送达开发人员的收件箱;恶意链接和任何脚本标签都显示为乱码文本,而不是可点击的元素。
关于 Gmail 的说明:该服务可能仍会将 URL 渲染为蓝色并使其可点击,但这属于客户端的便利功能,并不意味着 HTML 注入成功。修复方案阻止了注入创建真实的按钮或运行脚本。
开发人员应注意的事项
- 永远不要信任来自公开表单的数据 – 即使是看起来无害的表单,也可能产生在其他地方使用的存储输出。
- 在使用的点进行转义 – 在渲染前立即应用上下文相关的转义(文本 vs. 属性),而不是在流水线的更早阶段。
- 同时测试客户端和服务器端 – 单元测试可以发现明显的错误;通过正式 UI 进行的手动端到端测试可以确认防御措施在真实流量下依然有效。
- 注意邮件客户端的特性 – 某些客户端会自动将 URL 链接化,从而产生一种虚假的安全感。请务必验证底层 HTML 不包含任何活动元素。
总结
处理用户输入时的一个疏忽,就将常规的通知邮件变成了网络钓鱼的媒介。通过引入全面的 HTML 转义程序并在本地和生产环境中验证修复方案,恢复了收件箱的完整性。这一事件为任何从不可信数据生成 HTML 的 Web 应用提供了一个永恒的教训:适当的净化处理是不可逾越的底线,忽视它可能会直接为攻击者打开通往用户收件箱的大门。
