30-seconds-of-code HTML 安全指南:用 rel="noopener noreferrer" 杜绝 target="_blank" 的 tabnabbing 攻击

原创2026-09-30 23:10:311,368 阅读
文章标签:教程文档

30-seconds-of-code HTML 安全指南:用 rel="noopener noreferrer" 杜绝 target="_blank" 的 tabnabbing 攻击

在网页开发中,target="_blank" 是打开外部链接最常用的方式,但它同时携带一个容易被忽略的安全缺陷:新标签页可以通过 Window.opener 反向访问并篡改原页面。本篇基于 30-seconds-of-code 仓库中的 target-blank 片段,系统讲解该漏洞(tabnabbing)的成因、触发场景,并给出在项目里可立即落地的标准修复方案 rel="noopener noreferrer"。读完本文,你将掌握为所有新标签页链接加固的正确写法,并能结合仓库内已有的代码与工具完成批量审计。

背景:target="_blank" 的便利与隐患

当我们在页面中链接到外部资源时,target="_blank" 会让链接页面在新标签页(或新窗口)中打开,从而保留当前页面的浏览上下文,避免用户离开我们的站点:

<a href="https://externalresource.com/some-page" target="_blank">
  External resource
</a>

这种写法本身没有语法错误,但隐藏着一个安全风险:浏览器会为新打开的标签页建立一条指向"发起链接的页面"的反向引用。也就是说,新标签页里的脚本可以拿到我们页面的 Window.opener 引用,一旦它不可信,就能反过来做坏事。

风险原理:Window.opener 与 tabnabbing 攻击链

原文档明确指出,新标签页会通过 Window.opener 获得对链接来源页面的受限访问权限(limited access),并可以进一步通过 Window.opener.location 改写来源页面的 URL。这一攻击手法在业界被称为 tabnabbing(标签页劫持/钓鱼)。

完整的攻击链大致如下:

  1. 用户在我们的页面上点击一个 target="_blank" 的外部链接,新标签页打开恶意站点;
  2. 恶意站点通过 window.opener 拿到我们页面的引用;
  3. 恶意脚本将 window.opener.location 改写成钓鱼页面地址(如伪造的登录页);
  4. 用户切回原来的标签页时,看到的已是一个伪装成我们站点的钓鱼页面,可能在毫无防备的情况下输入账号密码。

由于用户在"新标签页里看内容"和"切回原标签页"之间存在时间差,这种替换往往难以察觉,这正是 tabnabbing 危险的地方。

哪些场景真正值得警惕

原文档特别强调:无论第三方资源看起来多么可信,我们都无法保证它永远安全。以下是需要格外谨慎的典型场景:

  • 外部资源本身不可信(来历不明的第三方站点);
  • 外部站点曾经被入侵或挂马(页面被植入恶意脚本);
  • 域名在多年间发生过所有权变更(老域名可能落入新主人之手);
  • 外部站点上存在用户上传内容(可能被注入脚本)。

换言之,安全性不能建立在"对方暂时可信"的假设上。作为开发者,我们必须默认外部链接是不可控的,并在代码层面把风险封死。

标准修复方案:rel="noopener noreferrer"

原文档给出的修复方案非常简单:为所有 target="_blank" 链接补上 rel="noopener noreferrer" 属性。完整示例对比如下:

<!-- Bad: susceptible to tabnabbing -->
<a href="https://externalresource.com/some-page" target="_blank">
  External resource
</a>

<!-- Good: new tab cannot cause problems  -->
<a
  href="https://externalresource.com/some-page"
  target="_blank"
  rel="noopener noreferrer"
>
  External resource
</a>

这段代码可以直接复制使用:只需在原有的 target="_blank" 链接上增加 rel="noopener noreferrer",新标签页便不再具备干扰来源页面的能力。

逐项拆解 rel 属性值

为便于理解并用于项目中的注释,这里把两个关键字的作用分开说明:

关键字 作用 说明
noopener 使新标签页的 window.opener 为 null 切断新页面到来源页面的反向引用,从根源上阻止 tabnabbing;这是防御核心
noreferrer 不再向目标站点发送 Referer 请求头 隐藏来源页面 URL,同时隐式包含 noopener 的效果

两点值得注意的细节:

  • noreferrer 在语义上已覆盖 noopener 的能力,但同时书写两者仍是社区与规范推荐的做法,既明确表达意图,也能在个别解析不完整的浏览器/中间环节中兜底;
  • 使用 noreferrer 会影响目标站点统计中的来源分析(Referer 丢失),这是隐私保护与数据统计之间的取舍,需要结合业务判断。

另外可以观察到,近年来主流浏览器的默认行为已逐渐把 target="_blank" 视为隐式携带 noopener 语义;但考虑到旧版本浏览器、代理环境和"显式优于隐式"的工程习惯,显式书写 rel="noopener noreferrer" 依然是当前最稳妥、可读性最高的做法。

仓库内的一致实践与交叉印证

这条规范不仅是文档层面的建议,30-seconds-of-code 仓库自身的代码也在严格执行:

此外,仓库中还有两篇与本文强关联的片段,可以配套阅读与使用:

  • open-all-links-in-new-tab 片段:教你用 <base target="_blank"> 让文档中所有链接统一在新标签页打开。该片段末尾明确警告了这种做法的安全风险,并直接链接到本篇 target-blank 片段。需要注意,<base> 元素只能统一设置 target 和 href,并不能为每个链接注入 rel 属性,因此在采用 <base target="_blank"> 的场景下,逐个链接补写 rel="noopener noreferrer" 反而更加必要;
  • query-selector-shorthand 片段:展示了用 $$('a[target="_blank"]') 一次性选中所有新标签页链接的写法。在做安全审计时,可以借助它快速枚举全站未加 rel 防护的 target="_blank" 链接,逐一补齐修复。

落地审计建议:一个可执行的检查清单

将原文档结论落到实际项目中,建议按以下顺序排查与加固:

  1. 静态排查:搜索代码中所有 target="_blank" 出现的位置,确认是否都带有 rel="noopener noreferrer";可使用仓库内 $$('a[target="_blank"]') 这类选择器在运行时复查页面中实际渲染的链接;
  2. 补齐属性:对缺失 rel 的链接统一补写 rel="noopener noreferrer",保持"新标签页打开"与"安全防护"同时生效;
  3. 覆盖 <base> 场景:如果文档头部使用了 <base target="_blank">,确认每个 <a> 标签仍显式携带 rel 防护,而不是依赖 <base> 统一处理;
  4. 纳入评审规范:把"所有 target="_blank" 必须带 rel="noopener noreferrer""写入代码评审清单,避免后续新链接回归风险。

小结

target="_blank" 带来的 tabnabbing 风险并不复杂,却实实在在威胁着用户的账户与隐私安全。它的修复同样简单——只需为链接追加 rel="noopener noreferrer",即可切断 Window.opener 反向引用,让新标签页"打不开来源页面的大门"。这条规则成本极低、收益明确,是每个前端项目都应当无条件落实的基础安全实践,也是 30-seconds-of-code 仓库自身在每一处外链上身体力行的标准。

登录后查看全文
30-seconds-of-code