为什么 rel='noopener' 对链接来说已经过时
为何 target=_blank 链接中的 rel=noopener 已过时,什么是反向标签劫持,以及何时仍需 noreferrer、opener 或 COOP。
对于一个普通的 target="_blank" 链接,rel="noopener" 现在是多余的:Chrome、Edge、Firefox 和 Safari 的所有当前版本都会自动应用 noopener 行为,因此仅写 target="_blank" 就已经会把 window.opener 设为 null。
如果你仍然出于肌肉记忆敲出它,或者盯着 linter 标记那个你忘记加的锚点,那你其实是在修补一个浏览器早在多年前就已经封堵的漏洞。本文将讲解它原本要防范的漏洞、浏览器在何时把该修复设为默认行为,以及 rel 仍然真正发挥作用的确切场景:noreferrer(并非自动生效)、rel="opener"(重新选择启用)以及用于站点级控制的 Cross-Origin-Opener-Policy 响应头。
关键要点
- 在现代浏览器上,仅写
target="_blank"就已经会将window.opener置为 null,因此手动添加rel="noopener"只是对浏览器已经封堵的漏洞做纵深防御。 - 隐式
noopener是分阶段落地的(Safari 在 2018–19 年,Firefox 79 在 2020 年中,Chromium 88 在 2021 年初),如今已成为 WHATWG HTML 标准的一部分。 - 根据 caniuse.com,隐式
noopener覆盖了全球约 95% 的浏览器使用量。 noreferrer不是隐式的:它仍会剥离Referer请求头,并且同时隐含noopener,因此只在你需要 referrer 隐私时才添加它。- 使用
rel="opener"重新启用window.opener,并使用Cross-Origin-Opener-Policy: same-origin在一处切断整个文档范围内的 opener 共享。
最初的问题:反向标签劫持(reverse tabnabbing)
在浏览器改变默认行为之前,target="_blank" 链接会把一个指向打开它的页面的实时引用交给新打开的页面。反向标签劫持(Reverse tabnabbing)就是利用这一点的攻击:目标页面读取 window.opener,在用户注意力集中在新标签页时,把原来的标签页重定向到一个钓鱼克隆站点。Mathias Bynens 对该问题的经典解释文章说得很直白:只要 window.opener 存在,被打开的页面就可以把 opener 引导到别处,无论两个页面各属于什么源。
这个漏洞利用在被打开的文档中只需一行代码:
if (window.opener) {
window.opener.location = 'https://you-re-hacked.com';
}
关键细节在于它可以跨源生效。当两个页面来自不同主机时,读写 window.opener.location 并不会被阻止,因此同源策略和 CORS 都无法阻挡对 opener 的重定向。这使得它在任何渲染用户生成内容或第三方链接的地方(论坛、评论、个人资料字段)都很危险——攻击者可以控制 href。
发生了什么变化:target=“_blank” 现在隐含 rel=“noopener”
Discover how at OpenReplay.com.
浏览器修正了默认行为。在 <a>、<area> 和 <form> 元素上,target="_blank" 现在与你自己写 rel="noopener" 具有相同效果:被打开的文档从 window.opener 得到的是 null,无需任何属性。该行为已写入 WHATWG HTML 规范,其跟随超链接的规则将任何 _blank 目标都视为 noopener,除非链接通过 rel="opener" 显式退出。OWASP 现在也把读者引向这一标准化的默认行为,并认为在常青浏览器上该攻击已基本被封堵。
这项变更历时约三年逐步铺开,所以”现代浏览器都这样做”是一条时间线,而不是某一个日期:
| 引擎 | 首个具备隐式 noopener 的稳定版本 | 大致发布时间 |
|---|---|---|
| Safari / WebKit | Safari 12.1(Tech Preview 68 中预览) | 2018 年末 – 2019 年 |
| Firefox / Gecko | Firefox 79 | 2020 年中 |
| Chromium(Chrome、Edge) | Chrome/Edge 88 | 2021 年初 |
请注意,这些版本描述的是隐式行为,而不是 rel="noopener" 属性本身开始被支持的时间。后者早了好几年,属于另一个里程碑。Caniuse 的隐式 noopener 表显示全球支持率约为 95%,常青浏览器自 2018 年左右起就已覆盖。剩下的那一小部分虽小但确实存在,所以在移除该属性之前先查看一下你自己的分析数据。值得注意的落后者是旧版非 Chromium 的 Edge。
手动添加已过时,并不等于毫无用处
rel="noopener" 手写起来是多余的,并不意味着整个 rel 属性没有意义。现在自动生效的关键字specifically是 noopener。其他关键字仍会改变行为:
| 关键字 | 作用 | 2026 年仍需手写吗? |
|---|---|---|
noopener | 将被打开页面中的 window.opener 置为 null | 不需要,target="_blank" 已隐含 |
noreferrer | 剥离 Referer 请求头并且隐含 noopener | 仅当你需要 referrer 隐私时 |
opener | 恢复 window.opener(重新启用) | 需要,当你确实需要该引用时 |
noreferrer 不是隐式的。它仍然会抑制 Referer 请求头,因此只在你确实想对目标页面隐瞒来源 URL 时才添加它。它还免费附带了安全好处:因为 noreferrer 同样会把 opener 置为 null,所以在它旁边再加 noopener 毫无意义。这使得常见的 rel="noopener noreferrer" 组合在现代浏览器上是双重冗余的,因为单独一个 noreferrer 就同时覆盖了两方面的关切。
如果你确实需要被打开的页面保留其 window.opener 引用(比如一个需要回传消息的弹窗),请使用 rel="opener" 显式选择启用。引入该变更的 WebKit 发布说明也是同样的表述:安全行为现在是默认的,而 rel="opener" 是你有意反转它的方式。
关于旧版支持有一点需要坦诚说明:无论如何添加 rel="noopener" 都是无害的。Chrome 的 Lighthouse 审计文档指出,把该属性写全对那些仍困在旧引擎(例如 Edge Legacy)上的用户仍能提供一些保护。它在现代浏览器上是噪音,但并不算错。
COOP:可扩展的站点级控制
要在一处切断整个文档范围内的 window.opener 共享,请发送 Cross-Origin-Opener-Policy: same-origin 响应头,而不是给每个链接都加装饰。Cross-Origin-Opener-Policy(COOP)响应头决定新打开的顶级文档是加入你的浏览上下文组(browsing context group),还是获得自己独立的一个。在 same-origin 下,跨源文档会落入一个独立的组,它们与其 opener 之间的引用被切断,从而在中心位置一次性关闭 opener 通道,而不是逐个锚点处理。
# nginx
add_header Cross-Origin-Opener-Policy "same-origin";
// Express
app.use((req, res, next) => {
res.set('Cross-Origin-Opener-Policy', 'same-origin');
next();
});
有一个限制:COOP 只能通过 HTTP 响应头交付。没有等效的 <meta http-equiv>,因此如果你的基础设施无法设置响应头,就无法应用 COOP。它在当前浏览器中已被广泛支持,作为纵深防御值得启用。对真实用户点击外部 target="_blank" 链接的会话回放(session replay)是一种实用手段,可以确认原标签页从未被导航过,也可以针对用户当时所用的确切浏览器复现任何”意外导航”的报告。
结论:现在该怎么做
对于面向现代浏览器的新代码,不要手动添加 rel="noopener";浏览器会为你设置。你可以安全地放宽那些强制在每个 target="_blank" 上加它的 lint 规则,例如 react/jsx-no-target-blank;只有在你必须覆盖 Edge Legacy 或其他 2021 年之前的引擎时才保留该规则。仅当你想抑制 Referer 请求头时,才添加 rel="noreferrer"。在少数确实需要拿回 opener 引用的情况下使用 rel="opener"。若想获得可扩展的、文档级的保证,请发送 Cross-Origin-Opener-Policy: same-origin。
值得指出:早期的指导意见——包括 OpenReplay 自己那篇建议在每个链接上都加 rel="noreferrer noopener" 的旧文——把 noopener 当作必须一直手写的东西,并未考虑隐式默认行为。这反映了一个在浏览器早已改变之后行业仍长期沿袭的习惯。2026 年准确的立场更为收窄:noopener 现在是默认行为,因此手动添加已经过时;而 noreferrer、rel="opener" 和 COOP 各自仍承担着不同的职责。有意识地使用它们,其余的交给浏览器处理。
常见问题
rel='noreferrer' 是否包含 rel='noopener'?
是的。设置 rel='noreferrer' 会自动隐含 rel='noopener',因此它在剥离 Referer 请求头之外,还会把 window.opener 置为 null。这意味着常见的 rel='noopener noreferrer' 组合在现代浏览器上是双重冗余的:单独的 noreferrer 就同时覆盖了 opener 置空和 referrer 抑制。只在你确实想对目标页面隐瞒来源 URL 时才添加 noreferrer。
如今在 target='_blank' 链接上完全省略 rel='noopener' 会发生什么?
在现代浏览器上不会有任何危险。仅写 target='_blank' 就已经将 window.opener 设为 null,因为 Chrome、Edge、Firefox 和 Safari 都隐式应用 noopener 行为,这一规则已被写入 WHATWG HTML 标准。Caniuse 显示其覆盖全球约 95% 的浏览器使用量。反向标签劫持在默认情况下已经失效;唯一的缺口是像非 Chromium 的 Edge Legacy 这类旧引擎。
我可以用 meta 标签代替响应头来设置 Cross-Origin-Opener-Policy 吗?
不行。COOP 只能通过 HTTP 响应头交付,没有等效的 meta http-equiv。如果你的基础设施无法设置响应头,就无法应用 COOP,只能退回到逐链接的 rel 属性或隐式 noopener 默认行为。当你能够设置响应头时,发送 Cross-Origin-Opener-Policy: same-origin 可以在一个中心位置切断整个文档范围内的 window.opener 共享,而不必给每个锚点都加装饰。
当我确实需要 window.opener 时,如何重新启用它?
在链接上显式使用 rel='opener'。由于将 window.opener 置为 null 的安全行为现在已是浏览器默认,rel='opener' 就是你有意恢复 opener 引用的方式,例如当弹窗需要向打开它的页面回传消息时。这会针对那个特定链接反转隐式 noopener 行为,而不影响其他链接。